diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e84b4..a769da9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,18 @@ callers). ### Changed +- **`SubtitleCandidate` renamed to `SubtitleScanResult`.** The old name + conflated "this might become a placed subtitle" with "this is what a + scan pass produced". The class is the output of a scan/identify pass + — language/format may still be `None`, confidence reflects how sure + the classifier is, and `raw_tokens` holds the filename fragments + under analysis. `SubtitleScanResult` says that directly. Pure rename + with a refreshed docstring in `alfred/domain/subtitles/entities.py`; + no behavior change. Touches the domain entity + `__init__` export, + the matcher / identifier / utils services, the manage_subtitles use + case, the placer, the metadata store, the shared-media cross-ref + comment, and the seven test modules that imported the type. + - **`ParsedRelease` is now frozen; enrichment passes return new instances.** The VO was mutable so `detect_media_type` and `enrich_from_probe` could patch fields in place — a code smell in a diff --git a/alfred/application/filesystem/manage_subtitles.py b/alfred/application/filesystem/manage_subtitles.py index 7f8c9f3..e191e61 100644 --- a/alfred/application/filesystem/manage_subtitles.py +++ b/alfred/application/filesystem/manage_subtitles.py @@ -4,7 +4,7 @@ import logging from pathlib import Path from alfred.domain.shared.value_objects import ImdbId -from alfred.domain.subtitles.entities import SubtitleCandidate +from alfred.domain.subtitles.entities import SubtitleScanResult from alfred.domain.subtitles.services.identifier import SubtitleIdentifier from alfred.domain.subtitles.services.matcher import SubtitleMatcher from alfred.domain.subtitles.services.pattern_detector import PatternDetector @@ -278,7 +278,7 @@ class ManageSubtitlesUseCase: def _to_unresolved_dto( - track: SubtitleCandidate, min_confidence: float = 0.7 + track: SubtitleScanResult, min_confidence: float = 0.7 ) -> UnresolvedTrack: reason = "unknown_language" if track.language is None else "low_confidence" return UnresolvedTrack( @@ -291,10 +291,10 @@ def _to_unresolved_dto( def _pair_placed_with_tracks( placed: list[PlacedTrack], - tracks: list[SubtitleCandidate], -) -> list[tuple[PlacedTrack, SubtitleCandidate]]: + tracks: list[SubtitleScanResult], +) -> list[tuple[PlacedTrack, SubtitleScanResult]]: """ - Pair each PlacedTrack with its originating SubtitleCandidate by source path. + Pair each PlacedTrack with its originating SubtitleScanResult by source path. Falls back to positional matching if paths don't align. """ track_by_path = {t.file_path: t for t in tracks if t.file_path} diff --git a/alfred/application/subtitles/placer.py b/alfred/application/subtitles/placer.py index 1f90100..b18994a 100644 --- a/alfred/application/subtitles/placer.py +++ b/alfred/application/subtitles/placer.py @@ -5,13 +5,13 @@ import os from dataclasses import dataclass from pathlib import Path -from alfred.domain.subtitles.entities import SubtitleCandidate +from alfred.domain.subtitles.entities import SubtitleScanResult from alfred.domain.subtitles.value_objects import SubtitleType logger = logging.getLogger(__name__) -def _build_dest_name(track: SubtitleCandidate, video_stem: str) -> str: +def _build_dest_name(track: SubtitleScanResult, video_stem: str) -> str: """ Build the destination filename for a subtitle track. @@ -41,7 +41,7 @@ class PlacedTrack: @dataclass class PlaceResult: placed: list[PlacedTrack] - skipped: list[tuple[SubtitleCandidate, str]] # (track, reason) + skipped: list[tuple[SubtitleScanResult, str]] # (track, reason) @property def placed_count(self) -> int: @@ -54,7 +54,7 @@ class PlaceResult: class SubtitlePlacer: """ - Hard-links matched SubtitleCandidate files next to a destination video. + Hard-links matched SubtitleScanResult files next to a destination video. Uses the same hard-link strategy as FileManager.copy_file: instant, no data duplication, qBittorrent keeps seeding. @@ -64,11 +64,11 @@ class SubtitlePlacer: def place( self, - tracks: list[SubtitleCandidate], + tracks: list[SubtitleScanResult], destination_video: Path, ) -> PlaceResult: placed: list[PlacedTrack] = [] - skipped: list[tuple[SubtitleCandidate, str]] = [] + skipped: list[tuple[SubtitleScanResult, str]] = [] dest_dir = destination_video.parent diff --git a/alfred/domain/shared/media.py b/alfred/domain/shared/media.py index e9ddcf4..f075fb1 100644 --- a/alfred/domain/shared/media.py +++ b/alfred/domain/shared/media.py @@ -3,7 +3,7 @@ These are the **container-view** dataclasses, populated from ffprobe output and used across the project to describe the content of a media file. -Not to be confused with ``alfred.domain.subtitles.entities.SubtitleCandidate`` +Not to be confused with ``alfred.domain.subtitles.entities.SubtitleScanResult`` which models a subtitle being **scanned/matched** (with confidence, raw tokens, file path, etc.). The two coexist by design — they describe the same real-world concept seen from two different bounded contexts. diff --git a/alfred/domain/subtitles/__init__.py b/alfred/domain/subtitles/__init__.py index 048cdd8..ed4c610 100644 --- a/alfred/domain/subtitles/__init__.py +++ b/alfred/domain/subtitles/__init__.py @@ -1,7 +1,7 @@ """Subtitles domain — subtitle identification, classification and placement.""" from .aggregates import SubtitleRuleSet -from .entities import MediaSubtitleMetadata, SubtitleCandidate +from .entities import MediaSubtitleMetadata, SubtitleScanResult from .exceptions import SubtitleNotFound from .services import PatternDetector, SubtitleIdentifier, SubtitleMatcher from .value_objects import ( @@ -17,7 +17,7 @@ from .value_objects import ( ) __all__ = [ - "SubtitleCandidate", + "SubtitleScanResult", "MediaSubtitleMetadata", "SubtitleRuleSet", "SubtitleIdentifier", diff --git a/alfred/domain/subtitles/entities.py b/alfred/domain/subtitles/entities.py index 7ecbfbe..46757a1 100644 --- a/alfred/domain/subtitles/entities.py +++ b/alfred/domain/subtitles/entities.py @@ -12,16 +12,18 @@ from .value_objects import ( @dataclass -class SubtitleCandidate: +class SubtitleScanResult: """ - A subtitle being scanned and matched — either an external file or an embedded stream. + A subtitle observed during a scan — either an external file or an embedded stream. Unlike ``alfred.domain.shared.media.SubtitleTrack`` (the pure container-view - populated from ffprobe), a SubtitleCandidate carries the **flow state** of the - subtitle matching pipeline: language/format are typed value objects that may - be ``None`` while classification is in progress, ``confidence`` reflects how - certain we are, and ``raw_tokens`` holds the filename fragments still under - analysis. State evolves: unknown → resolved after user clarification. + populated from ffprobe), a ``SubtitleScanResult`` carries the **flow state** + of the subtitle matching pipeline: language/format are typed value objects + that may be ``None`` while classification is in progress, ``confidence`` + reflects how certain we are, and ``raw_tokens`` holds the filename fragments + still under analysis. State evolves: unknown → resolved after user + clarification. The name reflects this — it's the **output of a scan pass**, + not a value object. """ # Classification (may be None if not yet resolved) @@ -72,7 +74,7 @@ class SubtitleCandidate: if self.is_embedded else str(self.file_path.name if self.file_path else "?") ) - return f"SubtitleCandidate({lang}, {self.subtitle_type.value}, {fmt}, src={src}, conf={self.confidence:.2f})" + return f"SubtitleScanResult({lang}, {self.subtitle_type.value}, {fmt}, src={src}, conf={self.confidence:.2f})" @dataclass @@ -84,14 +86,14 @@ class MediaSubtitleMetadata: media_id: ImdbId | None media_type: str # "movie" | "tv_show" - embedded_tracks: list[SubtitleCandidate] = field(default_factory=list) - external_tracks: list[SubtitleCandidate] = field(default_factory=list) + embedded_tracks: list[SubtitleScanResult] = field(default_factory=list) + external_tracks: list[SubtitleScanResult] = field(default_factory=list) release_group: str | None = None detected_pattern_id: str | None = None # pattern id from knowledge base pattern_confirmed: bool = False @property - def all_tracks(self) -> list[SubtitleCandidate]: + def all_tracks(self) -> list[SubtitleScanResult]: return self.embedded_tracks + self.external_tracks @property @@ -99,5 +101,5 @@ class MediaSubtitleMetadata: return len(self.embedded_tracks) + len(self.external_tracks) @property - def unresolved_tracks(self) -> list[SubtitleCandidate]: + def unresolved_tracks(self) -> list[SubtitleScanResult]: return [t for t in self.external_tracks if t.language is None] diff --git a/alfred/domain/subtitles/services/identifier.py b/alfred/domain/subtitles/services/identifier.py index bc98ccb..234d7fd 100644 --- a/alfred/domain/subtitles/services/identifier.py +++ b/alfred/domain/subtitles/services/identifier.py @@ -7,7 +7,7 @@ from pathlib import Path from ...shared.ports import FilesystemScanner, MediaProber from ..ports import SubtitleKnowledge from ...shared.value_objects import ImdbId -from ..entities import MediaSubtitleMetadata, SubtitleCandidate +from ..entities import MediaSubtitleMetadata, SubtitleScanResult from ..value_objects import ScanStrategy, SubtitlePattern, SubtitleType logger = logging.getLogger(__name__) @@ -94,7 +94,7 @@ class SubtitleIdentifier: # Embedded tracks — via MediaProber # ------------------------------------------------------------------ - def _scan_embedded(self, video_path: Path) -> list[SubtitleCandidate]: + def _scan_embedded(self, video_path: Path) -> list[SubtitleScanResult]: streams = self.prober.list_subtitle_streams(video_path) tracks = [] @@ -111,7 +111,7 @@ class SubtitleIdentifier: stype = SubtitleType.STANDARD tracks.append( - SubtitleCandidate( + SubtitleScanResult( language=lang, format=None, subtitle_type=stype, @@ -131,7 +131,7 @@ class SubtitleIdentifier: def _scan_external( self, video_path: Path, pattern: SubtitlePattern - ) -> list[SubtitleCandidate]: + ) -> list[SubtitleScanResult]: strategy = pattern.scan_strategy episode_stem: str | None = None @@ -200,7 +200,7 @@ class SubtitleIdentifier: entries: list, pattern: SubtitlePattern, episode_stem: str | None = None, - ) -> list[SubtitleCandidate]: + ) -> list[SubtitleScanResult]: tracks = [ self._classify_single(entry, episode_stem=episode_stem) for entry in entries ] @@ -214,7 +214,7 @@ class SubtitleIdentifier: def _classify_single( self, entry, episode_stem: str | None = None - ) -> SubtitleCandidate: + ) -> SubtitleScanResult: fmt = self.kb.format_for_extension(entry.suffix) tokens = ( _tokenize_suffix(entry.stem, episode_stem) @@ -253,7 +253,7 @@ class SubtitleIdentifier: if entry.suffix.lower() == ".srt": entry_count = _count_entries(self.scanner.read_text(entry.path)) - return SubtitleCandidate( + return SubtitleScanResult( language=language, format=fmt, subtitle_type=subtitle_type, @@ -266,8 +266,8 @@ class SubtitleIdentifier: ) def _disambiguate_by_size( - self, tracks: list[SubtitleCandidate] - ) -> list[SubtitleCandidate]: + self, tracks: list[SubtitleScanResult] + ) -> list[SubtitleScanResult]: """ 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 @@ -277,7 +277,7 @@ class SubtitleIdentifier: """ # Group by language code - lang_groups: dict[str, list[SubtitleCandidate]] = {} + lang_groups: dict[str, list[SubtitleScanResult]] = {} for track in tracks: key = track.language.code if track.language else "__unknown__" lang_groups.setdefault(key, []).append(track) @@ -306,6 +306,6 @@ class SubtitleIdentifier: return result - def _set_type(self, track: SubtitleCandidate, stype: SubtitleType) -> None: + def _set_type(self, track: SubtitleScanResult, stype: SubtitleType) -> None: """Mutate track type in-place.""" track.subtitle_type = stype diff --git a/alfred/domain/subtitles/services/matcher.py b/alfred/domain/subtitles/services/matcher.py index 27de2cf..fbd2375 100644 --- a/alfred/domain/subtitles/services/matcher.py +++ b/alfred/domain/subtitles/services/matcher.py @@ -2,7 +2,7 @@ import logging -from ..entities import SubtitleCandidate +from ..entities import SubtitleScanResult from ..value_objects import SubtitleMatchingRules logger = logging.getLogger(__name__) @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) class SubtitleMatcher: """ - Filters a list of SubtitleCandidate against effective SubtitleMatchingRules. + Filters a list of SubtitleScanResult against effective SubtitleMatchingRules. Returns matched tracks (pass all filters, confidence >= min_confidence) and unresolved tracks (need user clarification). @@ -21,14 +21,14 @@ class SubtitleMatcher: def match( self, - tracks: list[SubtitleCandidate], + tracks: list[SubtitleScanResult], rules: SubtitleMatchingRules, - ) -> tuple[list[SubtitleCandidate], list[SubtitleCandidate]]: + ) -> tuple[list[SubtitleScanResult], list[SubtitleScanResult]]: """ Returns (matched, unresolved). """ - matched: list[SubtitleCandidate] = [] - unresolved: list[SubtitleCandidate] = [] + matched: list[SubtitleScanResult] = [] + unresolved: list[SubtitleScanResult] = [] for track in tracks: if track.is_embedded: @@ -51,7 +51,7 @@ class SubtitleMatcher: return matched, unresolved def _passes_filters( - self, track: SubtitleCandidate, rules: SubtitleMatchingRules + self, track: SubtitleScanResult, rules: SubtitleMatchingRules ) -> bool: # Language filter if rules.preferred_languages: @@ -76,14 +76,14 @@ class SubtitleMatcher: def _resolve_conflicts( self, - tracks: list[SubtitleCandidate], + tracks: list[SubtitleScanResult], rules: SubtitleMatchingRules, - ) -> list[SubtitleCandidate]: + ) -> list[SubtitleScanResult]: """ When multiple tracks have same language + type, keep only the best one according to format_priority. If no format_priority applies, keep the first. """ - seen: dict[tuple, SubtitleCandidate] = {} + seen: dict[tuple, SubtitleScanResult] = {} for track in tracks: lang = track.language.code if track.language else None @@ -106,8 +106,8 @@ class SubtitleMatcher: def _prefer( self, - candidate: SubtitleCandidate, - existing: SubtitleCandidate, + candidate: SubtitleScanResult, + existing: SubtitleScanResult, format_priority: list[str], ) -> bool: """Return True if candidate is preferable to existing.""" diff --git a/alfred/domain/subtitles/services/utils.py b/alfred/domain/subtitles/services/utils.py index 526ac1c..0e29296 100644 --- a/alfred/domain/subtitles/services/utils.py +++ b/alfred/domain/subtitles/services/utils.py @@ -1,9 +1,9 @@ """Subtitle service utilities.""" -from ..entities import SubtitleCandidate +from ..entities import SubtitleScanResult -def available_subtitles(tracks: list[SubtitleCandidate]) -> list[SubtitleCandidate]: +def available_subtitles(tracks: list[SubtitleScanResult]) -> list[SubtitleScanResult]: """ Return the distinct subtitle tracks available, deduped by (language, type). @@ -11,7 +11,7 @@ def available_subtitles(tracks: list[SubtitleCandidate]) -> list[SubtitleCandida preferences — e.g. eng, eng.sdh, fra all show up as separate entries. """ seen: set[tuple] = set() - result: list[SubtitleCandidate] = [] + result: list[SubtitleScanResult] = [] for track in tracks: lang = track.language.code if track.language else None key = (lang, track.subtitle_type) diff --git a/alfred/infrastructure/subtitle/metadata_store.py b/alfred/infrastructure/subtitle/metadata_store.py index f2f3773..8c570e8 100644 --- a/alfred/infrastructure/subtitle/metadata_store.py +++ b/alfred/infrastructure/subtitle/metadata_store.py @@ -13,7 +13,7 @@ from datetime import UTC, datetime from pathlib import Path from typing import Any -from alfred.domain.subtitles.entities import SubtitleCandidate +from alfred.domain.subtitles.entities import SubtitleScanResult from alfred.application.subtitles.placer import PlacedTrack from alfred.infrastructure.metadata.store import MetadataStore @@ -25,7 +25,7 @@ class SubtitleMetadataStore: Subtitle-pipeline view of the per-release `.alfred/metadata.yaml`. Backed by a generic MetadataStore; this class only knows how to build - a subtitle_history entry from PlacedTrack/SubtitleCandidate pairs. + a subtitle_history entry from PlacedTrack/SubtitleScanResult pairs. """ def __init__(self, library_root: Path): @@ -45,7 +45,7 @@ class SubtitleMetadataStore: def append_history( self, - placed_pairs: list[tuple[PlacedTrack, SubtitleCandidate]], + placed_pairs: list[tuple[PlacedTrack, SubtitleScanResult]], season: int | None = None, episode: int | None = None, release_group: str | None = None, diff --git a/tests/application/test_manage_subtitles.py b/tests/application/test_manage_subtitles.py index c4aed58..f4f6a5d 100644 --- a/tests/application/test_manage_subtitles.py +++ b/tests/application/test_manage_subtitles.py @@ -40,7 +40,7 @@ from alfred.application.filesystem.manage_subtitles import ( _to_imdb_id, _to_unresolved_dto, ) -from alfred.domain.subtitles.entities import MediaSubtitleMetadata, SubtitleCandidate +from alfred.domain.subtitles.entities import MediaSubtitleMetadata, SubtitleScanResult from alfred.application.subtitles.placer import PlacedTrack, PlaceResult from alfred.domain.subtitles.value_objects import ( ScanStrategy, @@ -63,8 +63,8 @@ def _track( is_embedded: bool = False, raw_tokens: list[str] | None = None, file_size_kb: float | None = None, -) -> SubtitleCandidate: - return SubtitleCandidate( +) -> SubtitleScanResult: + return SubtitleScanResult( language=lang, format=fmt, subtitle_type=stype, diff --git a/tests/application/test_subtitle_placer.py b/tests/application/test_subtitle_placer.py index aecb1f0..7c14d2a 100644 --- a/tests/application/test_subtitle_placer.py +++ b/tests/application/test_subtitle_placer.py @@ -21,7 +21,7 @@ from unittest.mock import patch import pytest -from alfred.domain.subtitles.entities import SubtitleCandidate +from alfred.domain.subtitles.entities import SubtitleScanResult from alfred.application.subtitles.placer import ( PlacedTrack, PlaceResult, @@ -46,8 +46,8 @@ def _track( fmt=SRT, stype=SubtitleType.STANDARD, is_embedded: bool = False, -) -> SubtitleCandidate: - return SubtitleCandidate( +) -> SubtitleScanResult: + return SubtitleScanResult( language=lang, format=fmt, subtitle_type=stype, diff --git a/tests/domain/test_subtitle_identifier.py b/tests/domain/test_subtitle_identifier.py index 36251a7..92fa295 100644 --- a/tests/domain/test_subtitle_identifier.py +++ b/tests/domain/test_subtitle_identifier.py @@ -23,7 +23,7 @@ from unittest.mock import patch import pytest from alfred.domain.shared.ports import FileEntry -from alfred.domain.subtitles.entities import SubtitleCandidate +from alfred.domain.subtitles.entities import SubtitleScanResult from alfred.domain.subtitles.services.identifier import ( SubtitleIdentifier, _count_entries, @@ -310,8 +310,8 @@ class TestSizeDisambiguation: detection=TypeDetectionMethod.SIZE_AND_COUNT, ) - def _track(self, lang_code: str, entries: int) -> SubtitleCandidate: - return SubtitleCandidate( + def _track(self, lang_code: str, entries: int) -> SubtitleScanResult: + return SubtitleScanResult( language=SubtitleLanguage(code=lang_code, tokens=[lang_code]), format=None, subtitle_type=SubtitleType.UNKNOWN, diff --git a/tests/domain/test_subtitle_matcher.py b/tests/domain/test_subtitle_matcher.py index 59a02da..59cd287 100644 --- a/tests/domain/test_subtitle_matcher.py +++ b/tests/domain/test_subtitle_matcher.py @@ -18,7 +18,7 @@ from __future__ import annotations import pytest -from alfred.domain.subtitles.entities import SubtitleCandidate +from alfred.domain.subtitles.entities import SubtitleScanResult from alfred.domain.subtitles.services.matcher import SubtitleMatcher from alfred.domain.subtitles.value_objects import ( SubtitleFormat, @@ -40,8 +40,8 @@ def _track( stype: SubtitleType = SubtitleType.STANDARD, confidence: float = 1.0, is_embedded: bool = False, -) -> SubtitleCandidate: - return SubtitleCandidate( +) -> SubtitleScanResult: + return SubtitleScanResult( language=lang, format=fmt, subtitle_type=stype, diff --git a/tests/domain/test_subtitle_utils.py b/tests/domain/test_subtitle_utils.py index f07fff1..c7f3d21 100644 --- a/tests/domain/test_subtitle_utils.py +++ b/tests/domain/test_subtitle_utils.py @@ -5,9 +5,9 @@ uncovered: - ``TestSubtitleFormat`` — extension matching (case-insensitive). - ``TestSubtitleLanguage`` — token matching (case-insensitive). -- ``TestSubtitleCandidateDestName`` — ``destination_name`` property: +- ``TestSubtitleScanResultDestName`` — ``destination_name`` property: standard / SDH / forced naming, error on missing language or format. -- ``TestSubtitleCandidateRepr`` — debug repr for embedded vs external. +- ``TestSubtitleScanResultRepr`` — debug repr for embedded vs external. - ``TestMediaSubtitleMetadata`` — ``all_tracks`` / ``total_count`` / ``unresolved_tracks``. - ``TestAvailableSubtitles`` — utility dedup by (lang, type). @@ -24,7 +24,7 @@ from pathlib import Path import pytest from alfred.domain.subtitles.aggregates import SubtitleRuleSet -from alfred.domain.subtitles.entities import MediaSubtitleMetadata, SubtitleCandidate +from alfred.domain.subtitles.entities import MediaSubtitleMetadata, SubtitleScanResult from alfred.domain.subtitles.services.utils import available_subtitles from alfred.domain.subtitles.value_objects import ( RuleScope, @@ -74,7 +74,7 @@ class TestSubtitleLanguage: # --------------------------------------------------------------------------- # -# SubtitleCandidate # +# SubtitleScanResult # # --------------------------------------------------------------------------- # @@ -82,50 +82,50 @@ SRT = SubtitleFormat(id="srt", extensions=[".srt"]) FRA = SubtitleLanguage(code="fra", tokens=["fr", "fre"]) -class TestSubtitleCandidateDestName: +class TestSubtitleScanResultDestName: def test_standard(self): - t = SubtitleCandidate( + t = SubtitleScanResult( language=FRA, format=SRT, subtitle_type=SubtitleType.STANDARD ) assert t.destination_name == "fra.srt" def test_sdh(self): - t = SubtitleCandidate(language=FRA, format=SRT, subtitle_type=SubtitleType.SDH) + t = SubtitleScanResult(language=FRA, format=SRT, subtitle_type=SubtitleType.SDH) assert t.destination_name == "fra.sdh.srt" def test_forced(self): - t = SubtitleCandidate( + t = SubtitleScanResult( language=FRA, format=SRT, subtitle_type=SubtitleType.FORCED ) assert t.destination_name == "fra.forced.srt" def test_unknown_treated_as_standard(self): - t = SubtitleCandidate( + t = SubtitleScanResult( language=FRA, format=SRT, subtitle_type=SubtitleType.UNKNOWN ) # UNKNOWN doesn't add a suffix → same as standard. assert t.destination_name == "fra.srt" def test_missing_language_raises(self): - t = SubtitleCandidate(language=None, format=SRT) + t = SubtitleScanResult(language=None, format=SRT) with pytest.raises(ValueError, match="language or format missing"): t.destination_name def test_missing_format_raises(self): - t = SubtitleCandidate(language=FRA, format=None) + t = SubtitleScanResult(language=FRA, format=None) with pytest.raises(ValueError, match="language or format missing"): t.destination_name def test_extension_dot_stripped(self): # Format extension is ".srt" — leading dot must not be duplicated. - t = SubtitleCandidate(language=FRA, format=SRT) + t = SubtitleScanResult(language=FRA, format=SRT) assert t.destination_name.endswith(".srt") assert ".." not in t.destination_name -class TestSubtitleCandidateRepr: +class TestSubtitleScanResultRepr: def test_embedded_repr(self): - t = SubtitleCandidate( + t = SubtitleScanResult( language=FRA, format=None, is_embedded=True, confidence=1.0 ) r = repr(t) @@ -135,14 +135,14 @@ 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 = SubtitleScanResult(language=FRA, format=SRT, file_path=f, confidence=0.85) r = repr(t) assert "fra" in r assert "fr.srt" in r assert "0.85" in r def test_unresolved_repr(self): - t = SubtitleCandidate(language=None, format=None) + t = SubtitleScanResult(language=None, format=None) r = repr(t) assert "?" in r @@ -160,8 +160,8 @@ class TestMediaSubtitleMetadata: assert m.unresolved_tracks == [] def test_aggregates_embedded_and_external(self): - e = SubtitleCandidate(language=FRA, format=None, is_embedded=True) - x = SubtitleCandidate(language=FRA, format=SRT, file_path=Path("/x.srt")) + e = SubtitleScanResult(language=FRA, format=None, is_embedded=True) + x = SubtitleScanResult(language=FRA, format=SRT, file_path=Path("/x.srt")) m = MediaSubtitleMetadata( media_id=None, media_type="movie", @@ -174,13 +174,13 @@ 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( + embedded_unknown = SubtitleScanResult( language=None, format=None, is_embedded=True ) - external_known = SubtitleCandidate( + external_known = SubtitleScanResult( language=FRA, format=SRT, file_path=Path("/a.srt") ) - external_unknown = SubtitleCandidate( + external_unknown = SubtitleScanResult( language=None, format=SRT, file_path=Path("/b.srt") ) m = MediaSubtitleMetadata( @@ -201,14 +201,14 @@ class TestAvailableSubtitles: def test_dedup_by_lang_and_type(self): ENG = SubtitleLanguage(code="eng", tokens=["en"]) tracks = [ - SubtitleCandidate( + SubtitleScanResult( language=FRA, format=SRT, subtitle_type=SubtitleType.STANDARD ), - SubtitleCandidate( + SubtitleScanResult( language=FRA, format=SRT, subtitle_type=SubtitleType.STANDARD ), - SubtitleCandidate(language=FRA, format=SRT, subtitle_type=SubtitleType.SDH), - SubtitleCandidate( + SubtitleScanResult(language=FRA, format=SRT, subtitle_type=SubtitleType.SDH), + SubtitleScanResult( language=ENG, format=SRT, subtitle_type=SubtitleType.STANDARD ), ] @@ -222,10 +222,10 @@ class TestAvailableSubtitles: def test_none_language_treated_as_key(self): # Tracks with no language form a single None-keyed bucket. - t1 = SubtitleCandidate( + t1 = SubtitleScanResult( language=None, format=SRT, subtitle_type=SubtitleType.UNKNOWN ) - t2 = SubtitleCandidate( + t2 = SubtitleScanResult( language=None, format=SRT, subtitle_type=SubtitleType.UNKNOWN ) result = available_subtitles([t1, t2]) diff --git a/tests/infrastructure/test_subtitle_metadata_store.py b/tests/infrastructure/test_subtitle_metadata_store.py index 83a6233..5195348 100644 --- a/tests/infrastructure/test_subtitle_metadata_store.py +++ b/tests/infrastructure/test_subtitle_metadata_store.py @@ -16,7 +16,7 @@ from __future__ import annotations from pathlib import Path -from alfred.domain.subtitles.entities import SubtitleCandidate +from alfred.domain.subtitles.entities import SubtitleScanResult from alfred.application.subtitles.placer import PlacedTrack from alfred.domain.subtitles.value_objects import ( SubtitleFormat, @@ -32,8 +32,8 @@ ENG = SubtitleLanguage(code="eng", tokens=["en"]) def _track( lang=FRA, *, embedded: bool = False, confidence: float = 0.92 -) -> SubtitleCandidate: - return SubtitleCandidate( +) -> SubtitleScanResult: + return SubtitleScanResult( language=lang, format=SRT, subtitle_type=SubtitleType.STANDARD,