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:
+17
-13
@@ -19,14 +19,14 @@ if str(_PROJECT_ROOT) not in sys.path:
|
||||
# Colours
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
RESET = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
DIM = "\033[2m"
|
||||
GREEN = "\033[32m"
|
||||
RESET = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
DIM = "\033[2m"
|
||||
GREEN = "\033[32m"
|
||||
YELLOW = "\033[33m"
|
||||
RED = "\033[31m"
|
||||
CYAN = "\033[36m"
|
||||
BLUE = "\033[34m"
|
||||
RED = "\033[31m"
|
||||
CYAN = "\033[36m"
|
||||
BLUE = "\033[34m"
|
||||
|
||||
USE_COLOR = True
|
||||
|
||||
@@ -54,6 +54,7 @@ def hr() -> None:
|
||||
# Formatting helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def fmt_duration(seconds: float) -> str:
|
||||
h = int(seconds // 3600)
|
||||
m = int((seconds % 3600) // 60)
|
||||
@@ -80,6 +81,7 @@ def flag(val: bool) -> str:
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def main() -> None:
|
||||
global USE_COLOR
|
||||
|
||||
@@ -111,14 +113,14 @@ def main() -> None:
|
||||
|
||||
# --- Video ---
|
||||
section("Video")
|
||||
kv("codec", info.video_codec or c("—", DIM))
|
||||
kv("resolution", info.resolution or c("—", DIM))
|
||||
kv("codec", info.video_codec or c("—", DIM))
|
||||
kv("resolution", info.resolution or c("—", DIM))
|
||||
if info.width and info.height:
|
||||
kv("dimensions", f"{info.width} × {info.height}")
|
||||
if info.duration_seconds is not None:
|
||||
kv("duration", fmt_duration(info.duration_seconds))
|
||||
kv("duration", fmt_duration(info.duration_seconds))
|
||||
if info.bitrate_kbps is not None:
|
||||
kv("bitrate", f"{info.bitrate_kbps} kbps")
|
||||
kv("bitrate", f"{info.bitrate_kbps} kbps")
|
||||
|
||||
# --- Audio ---
|
||||
section(f"Audio {c(str(len(info.audio_tracks)) + ' track(s)', DIM)}")
|
||||
@@ -128,7 +130,7 @@ def main() -> None:
|
||||
lang = track.language or "und"
|
||||
default_marker = f" {c('default', GREEN, DIM)}" if track.is_default else ""
|
||||
print(f" {c(f'[{track.index}]', BOLD)} {c(lang, YELLOW)}{default_marker}")
|
||||
kv("codec", track.codec or c("—", DIM), indent=8)
|
||||
kv("codec", track.codec or c("—", DIM), indent=8)
|
||||
kv("channels", fmt_channels(track.channels, track.channel_layout), indent=8)
|
||||
|
||||
# --- Subtitles ---
|
||||
@@ -151,7 +153,9 @@ def main() -> None:
|
||||
hr()
|
||||
multi = c("yes", GREEN) if info.is_multi_audio else c("no", DIM)
|
||||
langs = ", ".join(info.audio_languages) if info.audio_languages else c("—", DIM)
|
||||
print(f" {c('multi-audio:', BOLD)} {multi} {c('languages:', BOLD)} {c(langs, CYAN)}")
|
||||
print(
|
||||
f" {c('multi-audio:', BOLD)} {multi} {c('languages:', BOLD)} {c(langs, CYAN)}"
|
||||
)
|
||||
hr()
|
||||
print()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user