chore: sprint cleanup — language unification, parser unification, fossils removal
Several weeks of work accumulated without being committed. Grouped here for clarity; see CHANGELOG.md [Unreleased] for the user-facing summary. Highlights ---------- P1 #2 — ISO 639-2/B canonical migration - New Language VO + LanguageRegistry (alfred/domain/shared/knowledge/). - iso_languages.yaml as single source of truth for language codes. - SubtitleKnowledgeBase now delegates lookup to LanguageRegistry; subtitles.yaml only declares subtitle-specific tokens (vostfr, vf, vff, …). - SubtitlePreferences default → ["fre", "eng"]; subtitle filenames written as {iso639_2b}.srt (legacy fr.srt still read via alias). - Scanner: dropped _LANG_KEYWORDS / _SDH_TOKENS / _FORCED_TOKENS / SUBTITLE_EXTENSIONS hardcoded dicts. - Fixed: 'hi' token no longer marks SDH (conflicted with Hindi alias). - Added settings.min_movie_size_bytes (was a module constant). P1 #3 — Release parser unification + data-driven tokenizer - parse_release() is now the single source of truth for release-name parsing. - alfred/knowledge/release/separators.yaml declares the token separators used by the tokenizer (., space, [, ], (, ), _). New conventions can be added without code changes. - Tokenizer now splits on any configured separator instead of name.split('.'). Releases like 'The Father (2020) [1080p] [WEBRip] [5.1] [YTS.MX]' parse via the direct path without sanitization fallback. - Site-tag extraction always runs first; well-formedness only rejects truly forbidden chars. - _parse_season_episode() extended with NxNN / NxNNxNN alt forms. - Removed dead helpers: _sanitize, _normalize. Domain cleanup - Deleted fossil services with zero production callers: alfred/domain/movies/services.py alfred/domain/tv_shows/services.py alfred/domain/subtitles/services.py (replaced by subtitles/services/ package) alfred/domain/subtitles/repositories.py - Split monolithic subtitle services into a package (identifier, matcher, placer, pattern_detector, utils) + dedicated knowledge/ package. - MediaInfo split into dedicated package (alfred/domain/shared/media/: audio, video, subtitle, info, matching). Persistence cleanup - Removed dead JSON repositories (movie/subtitle/tvshow_repository.py). Tests - Major expansion of the test suite organized to mirror the source tree. - Removed obsolete *_edge_cases test files superseded by structured tests. - Suite: 990 passed, 8 skipped. Misc - .gitignore: exclude env_backup/ and *.bak. - Adjustments across agent/llm, app.py, application/filesystem, and infrastructure/filesystem to align with the new domain layout.
This commit is contained in:
@@ -14,11 +14,12 @@ from alfred.domain.subtitles.scanner import (
|
||||
|
||||
|
||||
class TestClassify:
|
||||
def test_iso_lang_code(self, tmp_path):
|
||||
def test_iso_lang_code_639_1_alias(self, tmp_path):
|
||||
# ``fr`` is an alias of the canonical ISO 639-2/B code ``fre``.
|
||||
p = tmp_path / "fr.srt"
|
||||
p.write_text("")
|
||||
lang, is_sdh, is_forced = _classify(p)
|
||||
assert lang == "fr"
|
||||
assert lang == "fre"
|
||||
assert not is_sdh
|
||||
assert not is_forced
|
||||
|
||||
@@ -26,35 +27,39 @@ class TestClassify:
|
||||
p = tmp_path / "english.srt"
|
||||
p.write_text("")
|
||||
lang, _, _ = _classify(p)
|
||||
assert lang == "en"
|
||||
assert lang == "eng"
|
||||
|
||||
def test_french_keyword(self, tmp_path):
|
||||
p = tmp_path / "Show.S01E01.French.srt"
|
||||
p.write_text("")
|
||||
lang, _, _ = _classify(p)
|
||||
assert lang == "fr"
|
||||
assert lang == "fre"
|
||||
|
||||
def test_vostfr_is_french(self, tmp_path):
|
||||
p = tmp_path / "Show.S01E01.VOSTFR.srt"
|
||||
p.write_text("")
|
||||
lang, _, _ = _classify(p)
|
||||
assert lang == "fr"
|
||||
assert lang == "fre"
|
||||
|
||||
def test_sdh_token(self, tmp_path):
|
||||
p = tmp_path / "fr.sdh.srt"
|
||||
p = tmp_path / "fre.sdh.srt"
|
||||
p.write_text("")
|
||||
lang, is_sdh, _ = _classify(p)
|
||||
assert lang == "fr"
|
||||
assert lang == "fre"
|
||||
assert is_sdh
|
||||
|
||||
def test_hi_alias_for_sdh(self, tmp_path):
|
||||
def test_hi_no_longer_marks_sdh(self, tmp_path):
|
||||
# ``hi`` is the ISO 639-1 alias for Hindi; it must not mark a file as
|
||||
# SDH any more (regression of the previous collision between SDH and
|
||||
# Hindi tokens). Use ``sdh`` / ``cc`` / ``hearing`` to flag SDH instead.
|
||||
p = tmp_path / "en.hi.srt"
|
||||
p.write_text("")
|
||||
_, is_sdh, _ = _classify(p)
|
||||
assert is_sdh
|
||||
lang, is_sdh, _ = _classify(p)
|
||||
assert lang == "eng"
|
||||
assert not is_sdh
|
||||
|
||||
def test_forced_token(self, tmp_path):
|
||||
p = tmp_path / "fr.forced.srt"
|
||||
p = tmp_path / "fre.forced.srt"
|
||||
p.write_text("")
|
||||
_, _, is_forced = _classify(p)
|
||||
assert is_forced
|
||||
@@ -66,17 +71,17 @@ class TestClassify:
|
||||
assert lang is None
|
||||
|
||||
def test_dot_separator(self, tmp_path):
|
||||
p = tmp_path / "fr.sdh.srt"
|
||||
p = tmp_path / "fre.sdh.srt"
|
||||
p.write_text("")
|
||||
lang, is_sdh, _ = _classify(p)
|
||||
assert lang == "fr"
|
||||
assert lang == "fre"
|
||||
assert is_sdh
|
||||
|
||||
def test_hyphen_separator(self, tmp_path):
|
||||
p = tmp_path / "fr-forced.srt"
|
||||
p = tmp_path / "fre-forced.srt"
|
||||
p.write_text("")
|
||||
lang, _, is_forced = _classify(p)
|
||||
assert lang == "fr"
|
||||
assert lang == "fre"
|
||||
assert is_forced
|
||||
|
||||
|
||||
@@ -86,9 +91,9 @@ class TestClassify:
|
||||
|
||||
|
||||
class TestSubtitleCandidateDestinationName:
|
||||
def _make(self, lang="fr", is_sdh=False, is_forced=False, ext=".srt", path=None):
|
||||
def _make(self, lang="fre", is_sdh=False, is_forced=False, ext=".srt", path=None):
|
||||
return SubtitleCandidate(
|
||||
source_path=path or Path("/fake/fr.srt"),
|
||||
source_path=path or Path("/fake/fre.srt"),
|
||||
language=lang,
|
||||
is_sdh=is_sdh,
|
||||
is_forced=is_forced,
|
||||
@@ -96,19 +101,19 @@ class TestSubtitleCandidateDestinationName:
|
||||
)
|
||||
|
||||
def test_standard(self):
|
||||
assert self._make().destination_name == "fr.srt"
|
||||
assert self._make().destination_name == "fre.srt"
|
||||
|
||||
def test_sdh(self):
|
||||
assert self._make(is_sdh=True).destination_name == "fr.sdh.srt"
|
||||
assert self._make(is_sdh=True).destination_name == "fre.sdh.srt"
|
||||
|
||||
def test_forced(self):
|
||||
assert self._make(is_forced=True).destination_name == "fr.forced.srt"
|
||||
assert self._make(is_forced=True).destination_name == "fre.forced.srt"
|
||||
|
||||
def test_ass_extension(self):
|
||||
assert self._make(ext=".ass").destination_name == "fr.ass"
|
||||
assert self._make(ext=".ass").destination_name == "fre.ass"
|
||||
|
||||
def test_english_standard(self):
|
||||
assert self._make(lang="en").destination_name == "en.srt"
|
||||
assert self._make(lang="eng").destination_name == "eng.srt"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -119,7 +124,7 @@ class TestSubtitleCandidateDestinationName:
|
||||
class TestSubtitleScanner:
|
||||
def _scanner(self, languages=None, min_size_kb=0, keep_sdh=True, keep_forced=True):
|
||||
return SubtitleScanner(
|
||||
languages=languages or ["fr", "en"],
|
||||
languages=languages or ["fre", "eng"],
|
||||
min_size_kb=min_size_kb,
|
||||
keep_sdh=keep_sdh,
|
||||
keep_forced=keep_forced,
|
||||
@@ -131,31 +136,43 @@ class TestSubtitleScanner:
|
||||
return video
|
||||
|
||||
def test_finds_adjacent_subtitle(self, tmp_path):
|
||||
video = self._video(tmp_path)
|
||||
(tmp_path / "fre.srt").write_text("subtitle content")
|
||||
|
||||
candidates = self._scanner().scan(video)
|
||||
|
||||
assert len(candidates) == 1
|
||||
assert candidates[0].language == "fre"
|
||||
|
||||
def test_finds_adjacent_subtitle_legacy_639_1(self, tmp_path):
|
||||
# Reading existing media libraries: ``fr.srt`` is still recognized as
|
||||
# French and classified canonically as ``fre`` — covers user libraries
|
||||
# written before the ISO 639-2/B migration.
|
||||
video = self._video(tmp_path)
|
||||
(tmp_path / "fr.srt").write_text("subtitle content")
|
||||
|
||||
candidates = self._scanner().scan(video)
|
||||
|
||||
assert len(candidates) == 1
|
||||
assert candidates[0].language == "fr"
|
||||
assert candidates[0].language == "fre"
|
||||
|
||||
def test_finds_multiple_languages(self, tmp_path):
|
||||
video = self._video(tmp_path)
|
||||
(tmp_path / "fr.srt").write_text("fr subtitle")
|
||||
(tmp_path / "en.srt").write_text("en subtitle")
|
||||
(tmp_path / "fre.srt").write_text("fr subtitle")
|
||||
(tmp_path / "eng.srt").write_text("en subtitle")
|
||||
|
||||
candidates = self._scanner().scan(video)
|
||||
langs = {c.language for c in candidates}
|
||||
assert langs == {"fr", "en"}
|
||||
assert langs == {"fre", "eng"}
|
||||
|
||||
def test_scans_subs_subfolder(self, tmp_path):
|
||||
video = self._video(tmp_path)
|
||||
subs = tmp_path / "Subs"
|
||||
subs.mkdir()
|
||||
(subs / "fr.srt").write_text("subtitle")
|
||||
(subs / "fre.srt").write_text("subtitle")
|
||||
|
||||
candidates = self._scanner().scan(video)
|
||||
assert any(c.language == "fr" for c in candidates)
|
||||
assert any(c.language == "fre" for c in candidates)
|
||||
|
||||
def test_filters_unknown_language(self, tmp_path):
|
||||
video = self._video(tmp_path)
|
||||
@@ -166,14 +183,14 @@ class TestSubtitleScanner:
|
||||
|
||||
def test_filters_wrong_language(self, tmp_path):
|
||||
video = self._video(tmp_path)
|
||||
(tmp_path / "de.srt").write_text("german subtitle")
|
||||
(tmp_path / "ger.srt").write_text("german subtitle")
|
||||
|
||||
candidates = self._scanner(languages=["fr"]).scan(video)
|
||||
candidates = self._scanner(languages=["fre"]).scan(video)
|
||||
assert len(candidates) == 0
|
||||
|
||||
def test_filters_too_small_file(self, tmp_path):
|
||||
video = self._video(tmp_path)
|
||||
small = tmp_path / "fr.srt"
|
||||
small = tmp_path / "fre.srt"
|
||||
small.write_bytes(b"x") # 1 byte, well below any min_size_kb
|
||||
|
||||
candidates = self._scanner(min_size_kb=10).scan(video)
|
||||
@@ -181,21 +198,21 @@ class TestSubtitleScanner:
|
||||
|
||||
def test_filters_sdh_when_not_wanted(self, tmp_path):
|
||||
video = self._video(tmp_path)
|
||||
(tmp_path / "fr.sdh.srt").write_text("sdh subtitle")
|
||||
(tmp_path / "fre.sdh.srt").write_text("sdh subtitle")
|
||||
|
||||
candidates = self._scanner(keep_sdh=False).scan(video)
|
||||
assert len(candidates) == 0
|
||||
|
||||
def test_filters_forced_when_not_wanted(self, tmp_path):
|
||||
video = self._video(tmp_path)
|
||||
(tmp_path / "fr.forced.srt").write_text("forced subtitle")
|
||||
(tmp_path / "fre.forced.srt").write_text("forced subtitle")
|
||||
|
||||
candidates = self._scanner(keep_forced=False).scan(video)
|
||||
assert len(candidates) == 0
|
||||
|
||||
def test_keeps_sdh_when_wanted(self, tmp_path):
|
||||
video = self._video(tmp_path)
|
||||
(tmp_path / "fr.sdh.srt").write_text("sdh subtitle")
|
||||
(tmp_path / "fre.sdh.srt").write_text("sdh subtitle")
|
||||
|
||||
candidates = self._scanner(keep_sdh=True).scan(video)
|
||||
assert len(candidates) == 1
|
||||
@@ -203,8 +220,8 @@ class TestSubtitleScanner:
|
||||
|
||||
def test_ignores_non_subtitle_files(self, tmp_path):
|
||||
video = self._video(tmp_path)
|
||||
(tmp_path / "fr.nfo").write_text("nfo file")
|
||||
(tmp_path / "fr.jpg").write_bytes(b"image")
|
||||
(tmp_path / "fre.nfo").write_text("nfo file")
|
||||
(tmp_path / "fre.jpg").write_bytes(b"image")
|
||||
|
||||
candidates = self._scanner().scan(video)
|
||||
assert len(candidates) == 0
|
||||
|
||||
Reference in New Issue
Block a user