feat: split resolve_destination, persona-driven prompts, qBittorrent relocation
Destination resolution
- Replace the single ResolveDestinationUseCase with four dedicated
functions, one per release type:
resolve_season_destination (pack season, folder move)
resolve_episode_destination (single episode, file move)
resolve_movie_destination (movie, file move)
resolve_series_destination (multi-season pack, folder move)
- Each returns a dedicated DTO carrying only the fields relevant to
that release type — no more polymorphic ResolvedDestination with
half the fields unused depending on the case.
- Looser series folder matching: exact computed-name match is reused
silently; any deviation (different group, multiple candidates) now
prompts the user with all options including the computed name.
Agent tools
- Four new tools wrapping the use cases above; old resolve_destination
removed from the registry.
- New move_to_destination tool: create_folder + move, chained — used
after a resolve_* call to perform the actual relocation.
- Low-level filesystem_operations module (create_folder, move via mv)
for instant same-FS renames (ZFS).
Prompt & persona
- New PromptBuilder (alfred/agent/prompt.py) replacing prompts.py:
identity + personality block, situational expressions, memory
schema, episodic/STM/config context, tool catalogue.
- Per-user expression system: knowledge/users/common.yaml +
{username}.yaml are merged at runtime; one phrase per situation
(greeting/success/error/...) is sampled into the system prompt.
qBittorrent integration
- Credentials now come from settings (qbittorrent_url/username/password)
instead of hardcoded defaults.
- New client methods: find_by_name, set_location, recheck — the trio
needed to update a torrent's save path and re-verify after a move.
- Host→container path translation settings (qbittorrent_host_path /
qbittorrent_container_path) for docker-mounted setups.
Subtitles
- Identifier: strip parenthesized qualifiers (simplified, brazil…) at
tokenization; new _tokenize_suffix used for the episode_subfolder
pattern so episode-stem tokens no longer pollute language detection.
- Placer: extract _build_dest_name so it can be reused by the new
dry_run path in ManageSubtitlesUseCase.
- Knowledge: add yue, ell, ind, msa, rus, vie, heb, tam, tel, tha,
hin, ukr; add 'fre' to fra; add 'simplified'/'traditional' to zho.
Misc
- LTM workspace: add 'trash' folder slot.
- Default LLM provider switched to deepseek.
- testing/debug_release.py: CLI to parse a release, hit TMDB, and
dry-run the destination resolution end-to-end.
This commit is contained in:
@@ -10,21 +10,21 @@ _VIDEO_CODEC_MAP = {
|
||||
"hevc": "x265",
|
||||
"h264": "x264",
|
||||
"h265": "x265",
|
||||
"av1": "AV1",
|
||||
"vp9": "VP9",
|
||||
"av1": "AV1",
|
||||
"vp9": "VP9",
|
||||
"mpeg4": "XviD",
|
||||
}
|
||||
|
||||
# Map ffprobe audio codec names to scene-style tokens
|
||||
_AUDIO_CODEC_MAP = {
|
||||
"eac3": "EAC3",
|
||||
"ac3": "AC3",
|
||||
"dts": "DTS",
|
||||
"truehd": "TrueHD",
|
||||
"aac": "AAC",
|
||||
"flac": "FLAC",
|
||||
"opus": "OPUS",
|
||||
"mp3": "MP3",
|
||||
"eac3": "EAC3",
|
||||
"ac3": "AC3",
|
||||
"dts": "DTS",
|
||||
"truehd": "TrueHD",
|
||||
"aac": "AAC",
|
||||
"flac": "FLAC",
|
||||
"opus": "OPUS",
|
||||
"mp3": "MP3",
|
||||
"pcm_s16l": "PCM",
|
||||
"pcm_s24l": "PCM",
|
||||
}
|
||||
@@ -50,7 +50,9 @@ def enrich_from_probe(parsed: ParsedRelease, info: MediaInfo) -> None:
|
||||
parsed.quality = info.resolution
|
||||
|
||||
if parsed.codec is None and info.video_codec:
|
||||
parsed.codec = _VIDEO_CODEC_MAP.get(info.video_codec.lower(), info.video_codec.upper())
|
||||
parsed.codec = _VIDEO_CODEC_MAP.get(
|
||||
info.video_codec.lower(), info.video_codec.upper()
|
||||
)
|
||||
|
||||
if parsed.bit_depth is None and info.video_codec:
|
||||
# ffprobe exposes bit depth via pix_fmt — not in MediaInfo yet, skip for now
|
||||
@@ -62,10 +64,14 @@ def enrich_from_probe(parsed: ParsedRelease, info: MediaInfo) -> None:
|
||||
|
||||
if track:
|
||||
if parsed.audio_codec is None and track.codec:
|
||||
parsed.audio_codec = _AUDIO_CODEC_MAP.get(track.codec.lower(), track.codec.upper())
|
||||
parsed.audio_codec = _AUDIO_CODEC_MAP.get(
|
||||
track.codec.lower(), track.codec.upper()
|
||||
)
|
||||
|
||||
if parsed.audio_channels is None and track.channels:
|
||||
parsed.audio_channels = _CHANNEL_MAP.get(track.channels, f"{track.channels}ch")
|
||||
parsed.audio_channels = _CHANNEL_MAP.get(
|
||||
track.channels, f"{track.channels}ch"
|
||||
)
|
||||
|
||||
# Languages — merge ffprobe languages with token-level ones
|
||||
# "und" = undetermined, not useful
|
||||
|
||||
Reference in New Issue
Block a user