e45465d52d
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.
92 lines
2.6 KiB
Python
92 lines
2.6 KiB
Python
"""Subtitle domain value objects."""
|
|
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
|
|
|
|
class ScanStrategy(Enum):
|
|
"""How to locate subtitle files for a given release."""
|
|
|
|
ADJACENT = "adjacent" # .srt next to the video
|
|
FLAT = "flat" # Subs/*.srt
|
|
EPISODE_SUBFOLDER = "episode_subfolder" # Subs/{episode_name}/*.srt
|
|
EMBEDDED = "embedded" # tracks inside the video container
|
|
|
|
|
|
class TypeDetectionMethod(Enum):
|
|
"""How to differentiate standard / SDH / forced when tokens are ambiguous."""
|
|
|
|
TOKEN_IN_NAME = "token_in_name"
|
|
SIZE_AND_COUNT = "size_and_count"
|
|
FFPROBE_METADATA = "ffprobe_metadata"
|
|
|
|
|
|
class SubtitleType(Enum):
|
|
STANDARD = "standard"
|
|
SDH = "sdh"
|
|
FORCED = "forced"
|
|
UNKNOWN = "unknown"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SubtitleFormat:
|
|
"""A known subtitle file format."""
|
|
|
|
id: str
|
|
extensions: list[str]
|
|
description: str = ""
|
|
|
|
def matches_extension(self, ext: str) -> bool:
|
|
return ext.lower() in [e.lower() for e in self.extensions]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SubtitleLanguage:
|
|
"""A known subtitle language with its recognition tokens."""
|
|
|
|
code: str # ISO 639-1
|
|
tokens: list[str] # lowercase
|
|
|
|
def matches_token(self, token: str) -> bool:
|
|
return token.lower() in self.tokens
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SubtitlePattern:
|
|
"""
|
|
A known structural pattern for how a release group organises subtitle files.
|
|
|
|
Patterns are loaded from alfred/knowledge/patterns/*.yaml and are
|
|
independent of any specific release group — multiple groups can share
|
|
the same pattern.
|
|
"""
|
|
|
|
id: str
|
|
description: str
|
|
scan_strategy: ScanStrategy
|
|
root_folder: str | None # e.g. "Subs", None for adjacent/embedded
|
|
type_detection: TypeDetectionMethod
|
|
version: str = "1.0"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SubtitleMatchingRules:
|
|
"""
|
|
Effective rules after scope resolution (global → group → show → season → episode).
|
|
Only stores actual values — None means "inherited, not overridden at this level".
|
|
"""
|
|
|
|
preferred_languages: list[str] = field(default_factory=list) # ISO 639-1 codes
|
|
preferred_formats: list[str] = field(default_factory=list) # format ids
|
|
allowed_types: list[str] = field(default_factory=list) # SubtitleType ids
|
|
format_priority: list[str] = field(default_factory=list) # ordered format ids
|
|
min_confidence: float = 0.7
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RuleScope:
|
|
"""At which level a rule set applies."""
|
|
|
|
level: str # "global" | "release_group" | "movie" | "show" | "season" | "episode"
|
|
identifier: str | None = None # imdb_id, group name, "S01", "S01E03"…
|