refactor(subtitles): rename SubtitleCandidate → SubtitleScanResult

The old name conflated 'might become a placed subtitle' with 'what a
scan pass produced'. The class is the output of a scan/identify pass —
language/format may still be None while classification is in progress,
confidence reflects classifier certainty, raw_tokens holds filename
fragments under analysis. SubtitleScanResult says that directly.

Pure rename + refreshed docstring; no behavior change. Touches the
domain entity, the matcher/identifier/utils services, the
manage_subtitles use case, the placer, the metadata store, the
shared-media cross-ref comment, and 7 test modules.
This commit is contained in:
2026-05-21 08:05:46 +02:00
parent 5107cb32c0
commit 88f156b7a4
16 changed files with 111 additions and 97 deletions
+12
View File
@@ -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
@@ -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}
+6 -6
View File
@@ -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
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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",
+14 -12
View File
@@ -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]
+11 -11
View File
@@ -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
+12 -12
View File
@@ -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."""
+3 -3
View File
@@ -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)
@@ -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,
+3 -3
View File
@@ -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,
+3 -3
View File
@@ -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,
+3 -3
View File
@@ -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,
+3 -3
View File
@@ -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,
+27 -27
View File
@@ -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])
@@ -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,