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
+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])