From 891ba502a282a4f1eaa3c5c8fafc7606aca0037b Mon Sep 17 00:00:00 2001 From: Francwa Date: Sun, 17 May 2026 23:41:54 +0200 Subject: [PATCH] chore: apply pre-commit auto-fixes (trim trailing whitespace, EOF) --- alfred/agent/agent.py | 4 +- alfred/agent/prompt.py | 4 +- alfred/agent/tools/spec.py | 25 +++++--- .../filesystem/resolve_destination.py | 42 +++++++++---- .../shared/knowledge/language_registry.py | 11 +++- alfred/domain/shared/value_objects.py | 4 +- alfred/domain/subtitles/knowledge/base.py | 1 - .../domain/subtitles/services/identifier.py | 4 +- alfred/domain/tv_shows/entities.py | 35 ++++++++--- alfred/domain/tv_shows/value_objects.py | 4 +- .../memory/stm/components/tool_results.py | 4 +- .../infrastructure/subtitle/metadata_store.py | 4 +- tests/agent/test_ollama_client.py | 4 +- tests/application/test_enrich_from_probe.py | 12 +--- tests/application/test_manage_subtitles.py | 4 +- tests/application/test_resolve_destination.py | 61 +++++++++++++------ tests/domain/test_media_info.py | 30 ++++++--- tests/domain/test_subtitle_identifier.py | 11 ++-- tests/domain/test_subtitle_matcher.py | 24 ++------ tests/domain/test_subtitle_utils.py | 32 +++++++--- tests/domain/test_tv_shows.py | 14 ++--- .../api/test_qbittorrent_client.py | 12 +--- tests/infrastructure/api/test_tmdb_client.py | 10 +-- tests/infrastructure/test_metadata_store.py | 16 ++--- tests/infrastructure/test_rule_repository.py | 30 +++++---- .../test_subtitle_metadata_store.py | 4 +- 26 files changed, 238 insertions(+), 168 deletions(-) diff --git a/alfred/agent/agent.py b/alfred/agent/agent.py index 3a35461..1328455 100644 --- a/alfred/agent/agent.py +++ b/alfred/agent/agent.py @@ -192,9 +192,7 @@ class Agent: if cache_key_value is not None: cached = memory.stm.tool_results.get(tool_name, cache_key_value) if cached is not None: - logger.info( - f"Tool cache HIT: {tool_name}[{cache_key_value}]" - ) + logger.info(f"Tool cache HIT: {tool_name}[{cache_key_value}]") self._post_tool_side_effects(tool_name, args, cached, from_cache=True) return {**cached, "_from_cache": True} diff --git a/alfred/agent/prompt.py b/alfred/agent/prompt.py index 9a72c70..4280326 100644 --- a/alfred/agent/prompt.py +++ b/alfred/agent/prompt.py @@ -165,7 +165,9 @@ EXPRESSIONS À UTILISER (une par situation, naturellement intégrées dans ta r lines.append(" Steps:") for step in steps: step_id = step.get("id", "?") - step_tool = step.get("tool") or ("ask_user" if step.get("ask_user") else "—") + step_tool = step.get("tool") or ( + "ask_user" if step.get("ask_user") else "—" + ) lines.append(f" - {step_id} ({step_tool})") lines.append(" Call end_workflow(reason) when done, cancelled, or off-topic.") return "\n".join(lines) diff --git a/alfred/agent/tools/spec.py b/alfred/agent/tools/spec.py index 4c6e9b8..b9c47cb 100644 --- a/alfred/agent/tools/spec.py +++ b/alfred/agent/tools/spec.py @@ -27,9 +27,9 @@ class ToolSpecError(ValueError): class ParameterSpec: """Semantic description of a single tool parameter.""" - description: str # Short: what the value represents. - why_needed: str # Why the tool needs this — drives LLM reasoning. - example: str | None = None # Concrete example value, shown to the LLM. + description: str # Short: what the value represents. + why_needed: str # Why the tool needs this — drives LLM reasoning. + example: str | None = None # Concrete example value, shown to the LLM. @classmethod def from_dict(cls, name: str, data: dict) -> ParameterSpec: @@ -38,7 +38,9 @@ class ParameterSpec: return cls( description=str(data["description"]).strip(), why_needed=str(data["why_needed"]).strip(), - example=str(data["example"]).strip() if data.get("example") is not None else None, + example=str(data["example"]).strip() + if data.get("example") is not None + else None, ) @@ -54,7 +56,9 @@ class ReturnsSpec: _require(data, "description", f"returns.{key}") fields = data.get("fields") or {} if not isinstance(fields, dict): - raise ToolSpecError(f"returns.{key}.fields must be a dict, got {type(fields).__name__}") + raise ToolSpecError( + f"returns.{key}.fields must be a dict, got {type(fields).__name__}" + ) return cls( description=str(data["description"]).strip(), fields={str(k): str(v).strip() for k, v in fields.items()}, @@ -78,14 +82,14 @@ class ToolSpec: """Full semantic spec for one tool.""" name: str - summary: str # One-liner — becomes Tool.description. - description: str # Longer paragraph. + summary: str # One-liner — becomes Tool.description. + description: str # Longer paragraph. when_to_use: str when_not_to_use: str | None next_steps: str | None - parameters: dict[str, ParameterSpec] # name -> ParameterSpec - returns: dict[str, ReturnsSpec] # status_key -> ReturnsSpec - cache: CacheSpec | None = None # If present, tool is cached. + parameters: dict[str, ParameterSpec] # name -> ParameterSpec + returns: dict[str, ReturnsSpec] # status_key -> ReturnsSpec + cache: CacheSpec | None = None # If present, tool is cached. @classmethod def from_yaml_path(cls, path: Path) -> ToolSpec: @@ -200,6 +204,7 @@ class ToolSpec: # Helpers # --------------------------------------------------------------------------- + def _require(data: dict, key: str, where: str) -> None: if data.get(key) is None or (isinstance(data[key], str) and not data[key].strip()): raise ToolSpecError(f"{where}: missing required field '{key}'") diff --git a/alfred/application/filesystem/resolve_destination.py b/alfred/application/filesystem/resolve_destination.py index 2afa16c..2a5aeb3 100644 --- a/alfred/application/filesystem/resolve_destination.py +++ b/alfred/application/filesystem/resolve_destination.py @@ -29,7 +29,9 @@ def _sanitize(text: str) -> str: return _WIN_FORBIDDEN.sub("", text) -def _find_existing_tvshow_folders(tv_root: Path, tmdb_title: str, tmdb_year: int) -> list[str]: +def _find_existing_tvshow_folders( + tv_root: Path, tmdb_title: str, tmdb_year: int +) -> list[str]: """Return folder names in tv_root that match title + year prefix.""" if not tv_root.exists(): return [] @@ -52,9 +54,11 @@ def _get_tv_root() -> Path | None: # Internal sentinel + series-folder resolver (shared by the 3 TV use cases) # --------------------------------------------------------------------------- + @dataclass class _Clarification: """Module-private sentinel signalling that user input is needed.""" + question: str options: list[str] @@ -99,6 +103,7 @@ def _resolve_series_folder( # DTOs # --------------------------------------------------------------------------- + @dataclass class _ResolvedDestinationBase: """ @@ -109,7 +114,7 @@ class _ResolvedDestinationBase: and a to_dict() that delegates the non-ok cases via _base_dict(). """ - status: str # "ok" | "needs_clarification" | "error" + status: str # "ok" | "needs_clarification" | "error" # needs_clarification question: str | None = None @@ -124,7 +129,11 @@ class _ResolvedDestinationBase: if self.status == "error": return {"status": self.status, "error": self.error, "message": self.message} if self.status == "needs_clarification": - return {"status": self.status, "question": self.question, "options": self.options or []} + return { + "status": self.status, + "question": self.question, + "options": self.options or [], + } return None @@ -155,7 +164,7 @@ class ResolvedEpisodeDestination(_ResolvedDestinationBase): series_folder: str | None = None season_folder: str | None = None - library_file: str | None = None # full path to destination .mkv + library_file: str | None = None # full path to destination .mkv series_folder_name: str | None = None season_folder_name: str | None = None filename: str | None = None @@ -216,6 +225,7 @@ class ResolvedSeriesDestination(_ResolvedDestinationBase): # Use cases # --------------------------------------------------------------------------- + def resolve_season_destination( release_name: str, tmdb_title: str, @@ -231,14 +241,17 @@ def resolve_season_destination( tv_root = _get_tv_root() if not tv_root: return ResolvedSeasonDestination( - status="error", error="library_not_set", + status="error", + error="library_not_set", message="TV show library path is not configured.", ) parsed = parse_release(release_name) computed_name = _sanitize(parsed.show_folder_name(tmdb_title, tmdb_year)) - resolved = _resolve_series_folder(tv_root, tmdb_title, tmdb_year, computed_name, confirmed_folder) + resolved = _resolve_series_folder( + tv_root, tmdb_title, tmdb_year, computed_name, confirmed_folder + ) if isinstance(resolved, _Clarification): return ResolvedSeasonDestination( status="needs_clarification", @@ -277,7 +290,8 @@ def resolve_episode_destination( tv_root = _get_tv_root() if not tv_root: return ResolvedEpisodeDestination( - status="error", error="library_not_set", + status="error", + error="library_not_set", message="TV show library path is not configured.", ) @@ -285,7 +299,9 @@ def resolve_episode_destination( ext = Path(source_file).suffix computed_name = _sanitize(parsed.show_folder_name(tmdb_title, tmdb_year)) - resolved = _resolve_series_folder(tv_root, tmdb_title, tmdb_year, computed_name, confirmed_folder) + resolved = _resolve_series_folder( + tv_root, tmdb_title, tmdb_year, computed_name, confirmed_folder + ) if isinstance(resolved, _Clarification): return ResolvedEpisodeDestination( status="needs_clarification", @@ -328,7 +344,8 @@ def resolve_movie_destination( movies_root = memory.ltm.library_paths.get("movie") if not movies_root: return ResolvedMovieDestination( - status="error", error="library_not_set", + status="error", + error="library_not_set", message="Movie library path is not configured.", ) @@ -365,14 +382,17 @@ def resolve_series_destination( tv_root = _get_tv_root() if not tv_root: return ResolvedSeriesDestination( - status="error", error="library_not_set", + status="error", + error="library_not_set", message="TV show library path is not configured.", ) parsed = parse_release(release_name) computed_name = _sanitize(parsed.show_folder_name(tmdb_title, tmdb_year)) - resolved = _resolve_series_folder(tv_root, tmdb_title, tmdb_year, computed_name, confirmed_folder) + resolved = _resolve_series_folder( + tv_root, tmdb_title, tmdb_year, computed_name, confirmed_folder + ) if isinstance(resolved, _Clarification): return ResolvedSeriesDestination( status="needs_clarification", diff --git a/alfred/domain/shared/knowledge/language_registry.py b/alfred/domain/shared/knowledge/language_registry.py index af54bb1..e7fb264 100644 --- a/alfred/domain/shared/knowledge/language_registry.py +++ b/alfred/domain/shared/knowledge/language_registry.py @@ -75,8 +75,15 @@ class LanguageRegistry: self._load() def _load(self) -> None: - builtin = _load_yaml(_BUILTIN_ROOT / "iso_languages.yaml").get("languages", {}) or {} - learned = _load_yaml(_LEARNED_ROOT / "iso_languages_learned.yaml").get("languages", {}) or {} + builtin = ( + _load_yaml(_BUILTIN_ROOT / "iso_languages.yaml").get("languages", {}) or {} + ) + learned = ( + _load_yaml(_LEARNED_ROOT / "iso_languages_learned.yaml").get( + "languages", {} + ) + or {} + ) merged = _merge_language_entries(builtin, learned) for iso, entry in merged.items(): diff --git a/alfred/domain/shared/value_objects.py b/alfred/domain/shared/value_objects.py index 8b5ee82..cdab6b9 100644 --- a/alfred/domain/shared/value_objects.py +++ b/alfred/domain/shared/value_objects.py @@ -155,7 +155,9 @@ class Language: def __post_init__(self): if not isinstance(self.iso, str) or not self.iso: - raise ValidationError(f"Language.iso must be a non-empty string, got {self.iso!r}") + raise ValidationError( + f"Language.iso must be a non-empty string, got {self.iso!r}" + ) if len(self.iso) != 3: raise ValidationError( f"Language.iso must be a 3-letter ISO 639-2/B code, got {self.iso!r}" diff --git a/alfred/domain/subtitles/knowledge/base.py b/alfred/domain/subtitles/knowledge/base.py index c609187..2a5256a 100644 --- a/alfred/domain/subtitles/knowledge/base.py +++ b/alfred/domain/subtitles/knowledge/base.py @@ -35,7 +35,6 @@ class SubtitleKnowledgeBase: self._build() def _build(self) -> None: # noqa: PLR0912 — straight-line YAML projection - data = self._loader.subtitles() self._formats: dict[str, SubtitleFormat] = {} diff --git a/alfred/domain/subtitles/services/identifier.py b/alfred/domain/subtitles/services/identifier.py index 5d51230..276eb83 100644 --- a/alfred/domain/subtitles/services/identifier.py +++ b/alfred/domain/subtitles/services/identifier.py @@ -302,7 +302,9 @@ class SubtitleIdentifier: raw_tokens=tokens, ) - def _disambiguate_by_size(self, tracks: list[SubtitleCandidate]) -> list[SubtitleCandidate]: + def _disambiguate_by_size( + self, tracks: list[SubtitleCandidate] + ) -> list[SubtitleCandidate]: """ When multiple tracks share the same language and type is UNKNOWN/STANDARD, the one with the most entries (lines) is SDH, the smallest is FORCED if diff --git a/alfred/domain/tv_shows/entities.py b/alfred/domain/tv_shows/entities.py index b578972..dfac8d1 100644 --- a/alfred/domain/tv_shows/entities.py +++ b/alfred/domain/tv_shows/entities.py @@ -124,7 +124,9 @@ class Episode: return f"S{self.season_number.value:02d}E{self.episode_number.value:02d} - {self.title}" def __repr__(self) -> str: - return f"Episode(S{self.season_number.value:02d}E{self.episode_number.value:02d})" + return ( + f"Episode(S{self.season_number.value:02d}E{self.episode_number.value:02d})" + ) # ════════════════════════════════════════════════════════════════════════════ @@ -167,9 +169,7 @@ class Season: f"expected_episodes must be >= 0, got {self.expected_episodes}" ) if self.aired_episodes is not None and self.aired_episodes < 0: - raise ValueError( - f"aired_episodes must be >= 0, got {self.aired_episodes}" - ) + raise ValueError(f"aired_episodes must be >= 0, got {self.aired_episodes}") if ( self.expected_episodes is not None and self.aired_episodes is not None @@ -191,7 +191,11 @@ class Season: def _effective_aired(self) -> int | None: """``aired_episodes`` if set, else fall back to ``expected_episodes``.""" - return self.aired_episodes if self.aired_episodes is not None else self.expected_episodes + return ( + self.aired_episodes + if self.aired_episodes is not None + else self.expected_episodes + ) def is_complete(self) -> bool: """ @@ -259,7 +263,9 @@ class Season: return f"Season {self.season_number.value}" def __repr__(self) -> str: - return f"Season(number={self.season_number.value}, episodes={len(self.episodes)})" + return ( + f"Season(number={self.season_number.value}, episodes={len(self.episodes)})" + ) # ════════════════════════════════════════════════════════════════════════════ @@ -298,13 +304,17 @@ class TVShow: if isinstance(self.imdb_id, str): self.imdb_id = ImdbId(self.imdb_id) else: - raise ValueError(f"imdb_id must be ImdbId or str, got {type(self.imdb_id)}") + raise ValueError( + f"imdb_id must be ImdbId or str, got {type(self.imdb_id)}" + ) if not isinstance(self.status, ShowStatus): if isinstance(self.status, str): self.status = ShowStatus.from_string(self.status) else: - raise ValueError(f"status must be ShowStatus or str, got {type(self.status)}") + raise ValueError( + f"status must be ShowStatus or str, got {type(self.status)}" + ) if self.expected_seasons is not None and self.expected_seasons < 0: raise ValueError( @@ -380,7 +390,10 @@ class TVShow: # We also need to consider whether seasons themselves are missing. # If expected_seasons is known and we have fewer seasons than expected, # the missing seasons may have aired episodes → cannot claim COMPLETE. - if self.expected_seasons is not None and len(self.seasons) < self.expected_seasons: + if ( + self.expected_seasons is not None + and len(self.seasons) < self.expected_seasons + ): return CollectionStatus.PARTIAL return CollectionStatus.COMPLETE @@ -397,7 +410,9 @@ class TVShow: def missing_episodes(self) -> list[tuple[SeasonNumber, EpisodeNumber]]: """All aired-but-not-owned ``(season, episode)`` pairs across the show.""" result: list[tuple[SeasonNumber, EpisodeNumber]] = [] - for season_number, season in sorted(self.seasons.items(), key=lambda kv: kv[0].value): + for season_number, season in sorted( + self.seasons.items(), key=lambda kv: kv[0].value + ): for ep_number in season.missing_episodes(): result.append((season_number, ep_number)) return result diff --git a/alfred/domain/tv_shows/value_objects.py b/alfred/domain/tv_shows/value_objects.py index a282664..80ad481 100644 --- a/alfred/domain/tv_shows/value_objects.py +++ b/alfred/domain/tv_shows/value_objects.py @@ -104,8 +104,8 @@ class CollectionStatus(Enum): the TVShow aggregate as separate flags, not in this enum. """ - EMPTY = "empty" # 0 episode owned - PARTIAL = "partial" # some aired episodes are missing + EMPTY = "empty" # 0 episode owned + PARTIAL = "partial" # some aired episodes are missing COMPLETE = "complete" # all aired-to-date episodes are owned diff --git a/alfred/infrastructure/persistence/memory/stm/components/tool_results.py b/alfred/infrastructure/persistence/memory/stm/components/tool_results.py index 62eb51f..17e9da5 100644 --- a/alfred/infrastructure/persistence/memory/stm/components/tool_results.py +++ b/alfred/infrastructure/persistence/memory/stm/components/tool_results.py @@ -64,6 +64,4 @@ class ToolResultsCache: def to_dict(self) -> dict: # Surface only the index (tool + keys), not the payloads — payloads # can be large and the prompt only needs to know what's available. - return { - tool: list(bucket.keys()) for tool, bucket in self.results.items() - } + return {tool: list(bucket.keys()) for tool, bucket in self.results.items()} diff --git a/alfred/infrastructure/subtitle/metadata_store.py b/alfred/infrastructure/subtitle/metadata_store.py index 453ded5..e18f0cd 100644 --- a/alfred/infrastructure/subtitle/metadata_store.py +++ b/alfred/infrastructure/subtitle/metadata_store.py @@ -83,9 +83,7 @@ class SubtitleMetadataStore: entry["episode"] = episode self._store.append_subtitle_history_entry(entry) - marker = ( - f"S{season:02d}E{episode:02d}" if season and episode else "movie" - ) + marker = f"S{season:02d}E{episode:02d}" if season and episode else "movie" logger.info( f"SubtitleMetadataStore: appended history " f"({marker}) — {len(tracks_data)} track(s)" diff --git a/tests/agent/test_ollama_client.py b/tests/agent/test_ollama_client.py index a3eefce..664f3ce 100644 --- a/tests/agent/test_ollama_client.py +++ b/tests/agent/test_ollama_client.py @@ -185,9 +185,7 @@ class TestCompleteHappyPath: class TestCompleteErrors: def test_timeout_wrapped(self, client): - with patch( - "alfred.agent.llm.ollama.requests.post", side_effect=Timeout("t") - ): + with patch("alfred.agent.llm.ollama.requests.post", side_effect=Timeout("t")): with pytest.raises(LLMAPIError, match="timeout"): client.complete([{"role": "user", "content": "q"}]) diff --git a/tests/application/test_enrich_from_probe.py b/tests/application/test_enrich_from_probe.py index a5514d2..d40e21b 100644 --- a/tests/application/test_enrich_from_probe.py +++ b/tests/application/test_enrich_from_probe.py @@ -136,17 +136,13 @@ class TestAudio: assert p.audio_channels == "5.1" def test_channel_count_unknown_falls_back(self): - info = MediaInfo( - audio_tracks=[AudioTrack(0, "aac", 4, "quad", "eng")] - ) + info = MediaInfo(audio_tracks=[AudioTrack(0, "aac", 4, "quad", "eng")]) p = _bare() enrich_from_probe(p, info) assert p.audio_channels == "4ch" def test_unknown_audio_codec_uppercased(self): - info = MediaInfo( - audio_tracks=[AudioTrack(0, "newcodec", 2, "stereo", "eng")] - ) + info = MediaInfo(audio_tracks=[AudioTrack(0, "newcodec", 2, "stereo", "eng")]) p = _bare() enrich_from_probe(p, info) assert p.audio_codec == "NEWCODEC" @@ -158,9 +154,7 @@ class TestAudio: assert p.audio_channels is None def test_does_not_overwrite_existing_audio_fields(self): - info = MediaInfo( - audio_tracks=[AudioTrack(0, "ac3", 6, "5.1", "eng")] - ) + info = MediaInfo(audio_tracks=[AudioTrack(0, "ac3", 6, "5.1", "eng")]) p = _bare(audio_codec="DTS-HD.MA", audio_channels="7.1") enrich_from_probe(p, info) assert p.audio_codec == "DTS-HD.MA" diff --git a/tests/application/test_manage_subtitles.py b/tests/application/test_manage_subtitles.py index ec5cfab..29c6f76 100644 --- a/tests/application/test_manage_subtitles.py +++ b/tests/application/test_manage_subtitles.py @@ -142,7 +142,9 @@ class TestHelpers: def test_pair_placed_falls_back_to_positional(self): # Placed source path doesn't match any track.file_path → fallback uses tracks[0]. t = _track(file_path=Path("/in/known.srt")) - p = PlacedTrack(source=Path("/in/ghost.srt"), destination=Path("/x"), filename="x") + p = PlacedTrack( + source=Path("/in/ghost.srt"), destination=Path("/x"), filename="x" + ) pairs = _pair_placed_with_tracks([p], [t]) assert pairs == [(p, t)] diff --git a/tests/application/test_resolve_destination.py b/tests/application/test_resolve_destination.py index 2ecc652..ff7bc0c 100644 --- a/tests/application/test_resolve_destination.py +++ b/tests/application/test_resolve_destination.py @@ -78,7 +78,9 @@ class TestFindExistingTvshowFolders: (tmp_path / "oz.1997.bluray-OTHER").mkdir() (tmp_path / "OtherShow.1999").mkdir() out = _find_existing_tvshow_folders(tmp_path, "Oz", 1997) - assert out == ["Oz.1997.WEBRip-KONTRAST", "oz.1997.bluray-OTHER"] or set(out) == { + assert out == ["Oz.1997.WEBRip-KONTRAST", "oz.1997.bluray-OTHER"] or set( + out + ) == { "Oz.1997.WEBRip-KONTRAST", "oz.1997.bluray-OTHER", } @@ -103,28 +105,42 @@ class TestResolveSeriesFolderInternal: def test_confirmed_folder_when_exists(self, tmp_path): (tmp_path / "Oz.1997.X-GRP").mkdir() out = _resolve_series_folder( - tmp_path, "Oz", 1997, "Oz.1997.WEBRip-KONTRAST", confirmed_folder="Oz.1997.X-GRP" + tmp_path, + "Oz", + 1997, + "Oz.1997.WEBRip-KONTRAST", + confirmed_folder="Oz.1997.X-GRP", ) assert out == ("Oz.1997.X-GRP", False) def test_confirmed_folder_when_new(self, tmp_path): out = _resolve_series_folder( - tmp_path, "Oz", 1997, "Oz.1997.WEBRip-KONTRAST", confirmed_folder="Oz.1997.New-X" + tmp_path, + "Oz", + 1997, + "Oz.1997.WEBRip-KONTRAST", + confirmed_folder="Oz.1997.New-X", ) assert out == ("Oz.1997.New-X", True) def test_no_existing_returns_computed_as_new(self, tmp_path): - out = _resolve_series_folder(tmp_path, "Oz", 1997, "Oz.1997.WEBRip-KONTRAST", None) + out = _resolve_series_folder( + tmp_path, "Oz", 1997, "Oz.1997.WEBRip-KONTRAST", None + ) assert out == ("Oz.1997.WEBRip-KONTRAST", True) def test_single_existing_matching_computed_returns_existing(self, tmp_path): (tmp_path / "Oz.1997.WEBRip-KONTRAST").mkdir() - out = _resolve_series_folder(tmp_path, "Oz", 1997, "Oz.1997.WEBRip-KONTRAST", None) + out = _resolve_series_folder( + tmp_path, "Oz", 1997, "Oz.1997.WEBRip-KONTRAST", None + ) assert out == ("Oz.1997.WEBRip-KONTRAST", False) def test_single_existing_different_name_returns_clarification(self, tmp_path): (tmp_path / "Oz.1997.BluRay-OTHER").mkdir() - out = _resolve_series_folder(tmp_path, "Oz", 1997, "Oz.1997.WEBRip-KONTRAST", None) + out = _resolve_series_folder( + tmp_path, "Oz", 1997, "Oz.1997.WEBRip-KONTRAST", None + ) assert isinstance(out, _Clarification) assert "Oz" in out.question assert "Oz.1997.BluRay-OTHER" in out.options @@ -185,7 +201,9 @@ class TestSeason: assert out.series_folder_name.startswith("Oz.1997") assert out.season_folder_name.startswith("Oz.S03") assert out.series_folder == str(tv / out.series_folder_name) - assert out.season_folder == str(tv / out.series_folder_name / out.season_folder_name) + assert out.season_folder == str( + tv / out.series_folder_name / out.season_folder_name + ) def test_clarification_path(self, cfg_memory): _, tv, _ = cfg_memory @@ -222,32 +240,29 @@ class TestEpisode: ) def test_ok_path_without_episode_title(self, cfg_memory): - out = resolve_episode_destination( - REL_EPISODE, "/dl/source.mkv", "Oz", 1997 - ) + out = resolve_episode_destination(REL_EPISODE, "/dl/source.mkv", "Oz", 1997) assert out.status == "ok" # No '..' from blank ep title. assert ".." not in out.filename def test_extension_taken_from_source_file(self, cfg_memory): - out = resolve_episode_destination( - REL_EPISODE, "/dl/source.mp4", "Oz", 1997 - ) + out = resolve_episode_destination(REL_EPISODE, "/dl/source.mp4", "Oz", 1997) assert out.filename.endswith(".mp4") def test_clarification_path(self, cfg_memory): _, tv, _ = cfg_memory (tv / "Oz.1997.BluRay-OTHER").mkdir() - out = resolve_episode_destination( - REL_EPISODE, "/dl/source.mkv", "Oz", 1997 - ) + out = resolve_episode_destination(REL_EPISODE, "/dl/source.mkv", "Oz", 1997) assert out.status == "needs_clarification" def test_confirmed_folder_threaded_through(self, cfg_memory): _, tv, _ = cfg_memory (tv / "Oz.1997.BluRay-OTHER").mkdir() out = resolve_episode_destination( - REL_EPISODE, "/dl/source.mkv", "Oz", 1997, + REL_EPISODE, + "/dl/source.mkv", + "Oz", + 1997, confirmed_folder="Oz.1997.BluRay-OTHER", ) assert out.status == "ok" @@ -340,13 +355,21 @@ class TestDTOToDict: d = ResolvedSeasonDestination( status="error", error="library_not_set", message="missing" ).to_dict() - assert d == {"status": "error", "error": "library_not_set", "message": "missing"} + assert d == { + "status": "error", + "error": "library_not_set", + "message": "missing", + } def test_season_clarification(self): d = ResolvedSeasonDestination( status="needs_clarification", question="which?", options=["a", "b"] ).to_dict() - assert d == {"status": "needs_clarification", "question": "which?", "options": ["a", "b"]} + assert d == { + "status": "needs_clarification", + "question": "which?", + "options": ["a", "b"], + } def test_episode_ok(self): d = ResolvedEpisodeDestination( diff --git a/tests/domain/test_media_info.py b/tests/domain/test_media_info.py index 74a509d..9145572 100644 --- a/tests/domain/test_media_info.py +++ b/tests/domain/test_media_info.py @@ -20,8 +20,9 @@ from alfred.domain.shared.media import AudioTrack, MediaInfo, SubtitleTrack, Vid class TestTracks: def test_audio_track_defaults(self): - t = AudioTrack(index=0, codec="aac", channels=2, channel_layout="stereo", - language="eng") + t = AudioTrack( + index=0, codec="aac", channels=2, channel_layout="stereo", language="eng" + ) assert t.is_default is False def test_subtitle_track_defaults(self): @@ -36,7 +37,9 @@ class TestTracks: class TestVideoTrackResolution: def test_no_dimensions(self): - assert VideoTrack(index=0, codec=None, width=None, height=None).resolution is None + assert ( + VideoTrack(index=0, codec=None, width=None, height=None).resolution is None + ) @pytest.mark.parametrize( "w,expected", @@ -50,11 +53,16 @@ class TestVideoTrackResolution: ], ) def test_width_priority(self, w, expected): - assert VideoTrack(index=0, codec=None, width=w, height=1080).resolution == expected + assert ( + VideoTrack(index=0, codec=None, width=w, height=1080).resolution == expected + ) def test_widescreen_scope_crop(self): # 1920x960 (scope crop) → still 1080p because width-priority - assert VideoTrack(index=0, codec=None, width=1920, height=960).resolution == "1080p" + assert ( + VideoTrack(index=0, codec=None, width=1920, height=960).resolution + == "1080p" + ) @pytest.mark.parametrize( "h,expected", @@ -67,11 +75,15 @@ class TestVideoTrackResolution: ], ) def test_height_fallback_when_width_missing(self, h, expected): - assert VideoTrack(index=0, codec=None, width=None, height=h).resolution == expected + assert ( + VideoTrack(index=0, codec=None, width=None, height=h).resolution == expected + ) def test_width_below_buckets_falls_to_height(self): # width=320 falls below every bucket; falls back to f"{h}p" - assert VideoTrack(index=0, codec=None, width=320, height=240).resolution == "240p" + assert ( + VideoTrack(index=0, codec=None, width=320, height=240).resolution == "240p" + ) def test_width_only_below_buckets(self): # width=200, no height → f"{w}w" sentinel @@ -127,9 +139,7 @@ class TestAudioLanguages: assert m.audio_languages == [] def test_is_multi_audio_false_single_lang(self): - m = MediaInfo( - audio_tracks=[AudioTrack(0, "aac", 2, "stereo", "eng")] - ) + m = MediaInfo(audio_tracks=[AudioTrack(0, "aac", 2, "stereo", "eng")]) assert m.is_multi_audio is False def test_is_multi_audio_true(self): diff --git a/tests/domain/test_subtitle_identifier.py b/tests/domain/test_subtitle_identifier.py index 52ff0f0..65333f8 100644 --- a/tests/domain/test_subtitle_identifier.py +++ b/tests/domain/test_subtitle_identifier.py @@ -49,8 +49,11 @@ def identifier(kb): return SubtitleIdentifier(kb) -def _pattern(strategy: ScanStrategy, root_folder: str | None = None, - detection: TypeDetectionMethod = TypeDetectionMethod.TOKEN_IN_NAME) -> SubtitlePattern: +def _pattern( + strategy: ScanStrategy, + root_folder: str | None = None, + detection: TypeDetectionMethod = TypeDetectionMethod.TOKEN_IN_NAME, +) -> SubtitlePattern: return SubtitlePattern( id=f"test-{strategy.value}", description="", @@ -70,9 +73,7 @@ class TestTokenize: assert _tokenize("Show.S01E01.French") == ["show", "s01e01", "french"] def test_mixed_separators(self): - assert _tokenize("Show_S01-E01 French") == [ - "show", "s01", "e01", "french" - ] + assert _tokenize("Show_S01-E01 French") == ["show", "s01", "e01", "french"] def test_strips_parenthesized(self): assert _tokenize("episode (Brazil).French") == ["episode", "french"] diff --git a/tests/domain/test_subtitle_matcher.py b/tests/domain/test_subtitle_matcher.py index 777d566..59a02da 100644 --- a/tests/domain/test_subtitle_matcher.py +++ b/tests/domain/test_subtitle_matcher.py @@ -77,9 +77,7 @@ class TestUnresolved: def test_threshold_exact_passes(self, matcher): t = _track(confidence=0.7) - rules = SubtitleMatchingRules( - min_confidence=0.7, preferred_languages=["fra"] - ) + rules = SubtitleMatchingRules(min_confidence=0.7, preferred_languages=["fra"]) matched, unresolved = matcher.match([t], rules) assert matched == [t] @@ -92,17 +90,13 @@ class TestUnresolved: class TestLanguageFilter: def test_preferred_languages_filters_out(self, matcher): t_eng = _track(lang=ENG) - rules = SubtitleMatchingRules( - preferred_languages=["fra"], min_confidence=0.0 - ) + rules = SubtitleMatchingRules(preferred_languages=["fra"], min_confidence=0.0) matched, _ = matcher.match([t_eng], rules) assert matched == [] def test_preferred_language_match_passes(self, matcher): t_fra = _track(lang=FRA) - rules = SubtitleMatchingRules( - preferred_languages=["fra"], min_confidence=0.0 - ) + rules = SubtitleMatchingRules(preferred_languages=["fra"], min_confidence=0.0) matched, _ = matcher.match([t_fra], rules) assert matched == [t_fra] @@ -118,17 +112,13 @@ class TestLanguageFilter: class TestFormatFilter: def test_format_outside_preferred_filtered(self, matcher): t = _track(fmt=ASS) - rules = SubtitleMatchingRules( - preferred_formats=["srt"], min_confidence=0.0 - ) + rules = SubtitleMatchingRules(preferred_formats=["srt"], min_confidence=0.0) matched, _ = matcher.match([t], rules) assert matched == [] def test_no_format_attribute_filtered_when_pref_set(self, matcher): t = _track(fmt=None) - rules = SubtitleMatchingRules( - preferred_formats=["srt"], min_confidence=0.0 - ) + rules = SubtitleMatchingRules(preferred_formats=["srt"], min_confidence=0.0) matched, _ = matcher.match([t], rules) assert matched == [] @@ -144,9 +134,7 @@ class TestTypeFilter: def test_allowed_type_passes(self, matcher): t = _track(stype=SubtitleType.STANDARD) - rules = SubtitleMatchingRules( - allowed_types=["standard"], min_confidence=0.0 - ) + rules = SubtitleMatchingRules(allowed_types=["standard"], min_confidence=0.0) matched, _ = matcher.match([t], rules) assert matched == [t] diff --git a/tests/domain/test_subtitle_utils.py b/tests/domain/test_subtitle_utils.py index 75ff5fb..233b3b9 100644 --- a/tests/domain/test_subtitle_utils.py +++ b/tests/domain/test_subtitle_utils.py @@ -83,11 +83,15 @@ class TestSubtitleCandidateDestName: assert t.destination_name == "fra.sdh.srt" def test_forced(self): - t = SubtitleCandidate(language=FRA, format=SRT, subtitle_type=SubtitleType.FORCED) + t = SubtitleCandidate( + language=FRA, format=SRT, subtitle_type=SubtitleType.FORCED + ) assert t.destination_name == "fra.forced.srt" def test_unknown_treated_as_standard(self): - t = SubtitleCandidate(language=FRA, format=SRT, subtitle_type=SubtitleType.UNKNOWN) + t = SubtitleCandidate( + language=FRA, format=SRT, subtitle_type=SubtitleType.UNKNOWN + ) # UNKNOWN doesn't add a suffix → same as standard. assert t.destination_name == "fra.srt" @@ -110,7 +114,9 @@ class TestSubtitleCandidateDestName: class TestSubtitleCandidateRepr: def test_embedded_repr(self): - t = SubtitleCandidate(language=FRA, format=None, is_embedded=True, confidence=1.0) + t = SubtitleCandidate( + language=FRA, format=None, is_embedded=True, confidence=1.0 + ) r = repr(t) assert "fra" in r assert "embedded" in r @@ -118,9 +124,7 @@ class TestSubtitleCandidateRepr: def test_external_repr_uses_filename(self, tmp_path): f = tmp_path / "fr.srt" f.write_text("") - t = SubtitleCandidate( - language=FRA, format=SRT, file_path=f, confidence=0.85 - ) + t = SubtitleCandidate(language=FRA, format=SRT, file_path=f, confidence=0.85) r = repr(t) assert "fra" in r assert "fr.srt" in r @@ -159,7 +163,9 @@ class TestMediaSubtitleMetadata: def test_unresolved_tracks_only_external_with_none_lang(self): # An embedded with None language must NOT appear in unresolved_tracks # (the property only iterates external_tracks). - embedded_unknown = SubtitleCandidate(language=None, format=None, is_embedded=True) + embedded_unknown = SubtitleCandidate( + language=None, format=None, is_embedded=True + ) external_known = SubtitleCandidate( language=FRA, format=SRT, file_path=Path("/a.srt") ) @@ -184,10 +190,16 @@ class TestAvailableSubtitles: def test_dedup_by_lang_and_type(self): ENG = SubtitleLanguage(code="eng", tokens=["en"]) tracks = [ - SubtitleCandidate(language=FRA, format=SRT, subtitle_type=SubtitleType.STANDARD), - SubtitleCandidate(language=FRA, format=SRT, subtitle_type=SubtitleType.STANDARD), + SubtitleCandidate( + language=FRA, format=SRT, subtitle_type=SubtitleType.STANDARD + ), + SubtitleCandidate( + language=FRA, format=SRT, subtitle_type=SubtitleType.STANDARD + ), SubtitleCandidate(language=FRA, format=SRT, subtitle_type=SubtitleType.SDH), - SubtitleCandidate(language=ENG, format=SRT, subtitle_type=SubtitleType.STANDARD), + SubtitleCandidate( + language=ENG, format=SRT, subtitle_type=SubtitleType.STANDARD + ), ] result = available_subtitles(tracks) keys = [(t.language.code, t.subtitle_type) for t in result] diff --git a/tests/domain/test_tv_shows.py b/tests/domain/test_tv_shows.py index 07abe8a..e5fb35f 100644 --- a/tests/domain/test_tv_shows.py +++ b/tests/domain/test_tv_shows.py @@ -182,11 +182,13 @@ class TestEpisode: assert e.has_audio_in("ger") is False def test_has_audio_in_with_language(self): - lang = Language(iso="fre", english_name="French", native_name="Français", - aliases=("fr", "fra", "french")) - e = self._ep( - audio_tracks=[AudioTrack(0, "ac3", 6, "5.1", "fr")] + lang = Language( + iso="fre", + english_name="French", + native_name="Français", + aliases=("fr", "fra", "french"), ) + e = self._ep(audio_tracks=[AudioTrack(0, "ac3", 6, "5.1", "fr")]) # str query "fre" wouldn't match "fr" directly — but Language does cross-format assert e.has_audio_in(lang) is True assert e.has_audio_in("fre") is False # direct compare misses @@ -205,9 +207,7 @@ class TestEpisode: # ── Subtitle helpers ─────────────────────────────────────────────── def test_has_subtitles_in(self): - e = self._ep( - subtitle_tracks=[SubtitleTrack(0, "subrip", "fre")] - ) + e = self._ep(subtitle_tracks=[SubtitleTrack(0, "subrip", "fre")]) assert e.has_subtitles_in("fre") is True assert e.has_subtitles_in("eng") is False diff --git a/tests/infrastructure/api/test_qbittorrent_client.py b/tests/infrastructure/api/test_qbittorrent_client.py index 4d8c7eb..c07d4dd 100644 --- a/tests/infrastructure/api/test_qbittorrent_client.py +++ b/tests/infrastructure/api/test_qbittorrent_client.py @@ -79,9 +79,7 @@ def client(): class TestInit: def test_explicit_args(self): - c = QBittorrentClient( - host="http://x:1", username="u", password="p", timeout=99 - ) + c = QBittorrentClient(host="http://x:1", username="u", password="p", timeout=99) assert c.host == "http://x:1" assert c.username == "u" assert c.password == "p" @@ -265,9 +263,7 @@ class TestMutations: def test_delete_no_files_default(self, client): self._ok(client) client.delete_torrent("hash1") - assert ( - client.session.post.call_args.kwargs["data"]["deleteFiles"] == "false" - ) + assert client.session.post.call_args.kwargs["data"]["deleteFiles"] == "false" def test_pause(self, client): self._ok(client) @@ -328,9 +324,7 @@ class TestFindByName: def test_case_insensitive_match(self, client): client._authenticated = True - client.session.get.return_value = _resp( - [_torrent_dict("foundation.s01")] - ) + client.session.get.return_value = _resp([_torrent_dict("foundation.s01")]) result = client.find_by_name("Foundation.S01") assert result is not None assert result.name == "foundation.s01" diff --git a/tests/infrastructure/api/test_tmdb_client.py b/tests/infrastructure/api/test_tmdb_client.py index 1f3a67e..add753b 100644 --- a/tests/infrastructure/api/test_tmdb_client.py +++ b/tests/infrastructure/api/test_tmdb_client.py @@ -231,11 +231,7 @@ class TestSearchMedia: def test_tv_uses_name_field(self, mock_get, client): mock_get.side_effect = [ _ok_response( - { - "results": [ - {"id": 1396, "media_type": "tv", "name": "Breaking Bad"} - ] - } + {"results": [{"id": 1396, "media_type": "tv", "name": "Breaking Bad"}]} ), _ok_response({"imdb_id": "tt0903747"}), ] @@ -281,9 +277,7 @@ class TestSearchMedia: def test_external_ids_failure_returns_result_without_imdb(self, mock_get, client): # Second call (external IDs) fails — the search should still succeed. mock_get.side_effect = [ - _ok_response( - {"results": [{"id": 1, "media_type": "movie", "title": "X"}]} - ), + _ok_response({"results": [{"id": 1, "media_type": "movie", "title": "X"}]}), Timeout("slow"), ] result = client.search_media("X") diff --git a/tests/infrastructure/test_metadata_store.py b/tests/infrastructure/test_metadata_store.py index 7830377..7e533a1 100644 --- a/tests/infrastructure/test_metadata_store.py +++ b/tests/infrastructure/test_metadata_store.py @@ -160,13 +160,15 @@ class TestUpdateParseAndProbe: class TestUpdateTmdb: def test_promotes_identity_to_top_level(self, tmp_path): s = MetadataStore(tmp_path) - s.update_tmdb({ - "status": "ok", - "imdb_id": "tt1375666", - "tmdb_id": 27205, - "media_type": "movie", - "title": "Inception", - }) + s.update_tmdb( + { + "status": "ok", + "imdb_id": "tt1375666", + "tmdb_id": 27205, + "media_type": "movie", + "title": "Inception", + } + ) data = s.load() assert data["imdb_id"] == "tt1375666" assert data["tmdb_id"] == 27205 diff --git a/tests/infrastructure/test_rule_repository.py b/tests/infrastructure/test_rule_repository.py index 247e39e..e5d2027 100644 --- a/tests/infrastructure/test_rule_repository.py +++ b/tests/infrastructure/test_rule_repository.py @@ -38,17 +38,23 @@ def _write(path: Path, data: dict) -> None: class TestFilterOverride: def test_keeps_only_valid_keys(self): - out = _filter_override({ - "languages": ["fra"], - "formats": ["srt"], - "types": ["standard"], - "format_priority": ["srt"], - "min_confidence": 0.8, - "unknown_key": "ignored", - "another": 42, - }) + out = _filter_override( + { + "languages": ["fra"], + "formats": ["srt"], + "types": ["standard"], + "format_priority": ["srt"], + "min_confidence": 0.8, + "unknown_key": "ignored", + "another": 42, + } + ) assert set(out) == { - "languages", "formats", "types", "format_priority", "min_confidence" + "languages", + "formats", + "types", + "format_priority", + "min_confidence", } assert "unknown_key" not in out @@ -113,9 +119,7 @@ class TestLoad: {"override": {"min_confidence": 0.99}}, ) repo = RuleSetRepository(tmp_path) - rules = repo.load( - release_group="GRP", subtitle_preferences=prefs - ).resolve() + rules = repo.load(release_group="GRP", subtitle_preferences=prefs).resolve() # All three levels visible — local overrides on top assert rules.preferred_languages == ["jpn"] assert rules.format_priority == ["ass"] diff --git a/tests/infrastructure/test_subtitle_metadata_store.py b/tests/infrastructure/test_subtitle_metadata_store.py index 8c3e8f6..716af12 100644 --- a/tests/infrastructure/test_subtitle_metadata_store.py +++ b/tests/infrastructure/test_subtitle_metadata_store.py @@ -30,7 +30,9 @@ FRA = SubtitleLanguage(code="fra", tokens=["fr"]) ENG = SubtitleLanguage(code="eng", tokens=["en"]) -def _track(lang=FRA, *, embedded: bool = False, confidence: float = 0.92) -> SubtitleCandidate: +def _track( + lang=FRA, *, embedded: bool = False, confidence: float = 0.92 +) -> SubtitleCandidate: return SubtitleCandidate( language=lang, format=SRT,