refactor(subtitles): introduce SubtitleKnowledge Protocol port

Domain services (SubtitleIdentifier, PatternDetector) used to import the
concrete SubtitleKnowledgeBase class directly from infrastructure for
their type hint. With this commit they depend on a structural Protocol
in alfred/domain/subtitles/ports/knowledge.py declaring just the 7
read-only query methods the domain actually consumes.

The concrete YAML-backed SubtitleKnowledgeBase in infrastructure remains
the sole adapter — no rename, no shim. With this change
alfred/domain/subtitles/ has zero imports from alfred/infrastructure/.

Also extend the changelog entry covering the full domain-io-extraction
branch.
This commit is contained in:
2026-05-19 15:15:43 +02:00
parent 535935cc73
commit df798f55cc
5 changed files with 59 additions and 6 deletions
+11
View File
@@ -143,6 +143,17 @@ callers).
an explicit `default_rules: SubtitleMatchingRules` parameter. The an explicit `default_rules: SubtitleMatchingRules` parameter. The
`ManageSubtitles` use case loads defaults from the KB once and `ManageSubtitles` use case loads defaults from the KB once and
passes them in. passes them in.
- **`SubtitleKnowledge` Protocol port** at
`alfred/domain/subtitles/ports/knowledge.py` declares the read-only
query surface domain services consume (7 methods:
`known_extensions`, `format_for_extension`, `language_for_token`,
`is_known_lang_token`, `type_for_token`, `is_known_type_token`,
`patterns`). `SubtitleIdentifier` and `PatternDetector` depend on
this Protocol instead of the concrete `SubtitleKnowledgeBase` from
infrastructure — `domain/subtitles/` now has zero imports from
`infrastructure/`. The remaining domain → infra leak
(`domain/release/` loading separator YAML at import-time) is
documented in tech-debt and scheduled for its own branch.
- **`to_dot_folder_name(title)` helper** in - **`to_dot_folder_name(title)` helper** in
`alfred/domain/shared/value_objects.py` — extracts the `alfred/domain/shared/value_objects.py` — extracts the
`re.sub(r"[^\w\s\.\-]", "", title).replace(" ", ".")` pattern that was `re.sub(r"[^\w\s\.\-]", "", title).replace(" ", ".")` pattern that was
@@ -0,0 +1,6 @@
"""Domain ports for the subtitles domain — Protocol-based abstractions
that decouple domain services from concrete infrastructure adapters."""
from .knowledge import SubtitleKnowledge
__all__ = ["SubtitleKnowledge"]
@@ -0,0 +1,38 @@
"""SubtitleKnowledge port — the query surface domain services need from the
subtitle knowledge base, expressed as a Protocol so the domain never imports
the infrastructure adapter that backs it.
The concrete implementation lives in
``alfred/infrastructure/knowledge/subtitles/base.py`` (the YAML-backed
``SubtitleKnowledgeBase``). Tests can supply any object that satisfies this
structural contract.
"""
from __future__ import annotations
from typing import Protocol
from ..value_objects import SubtitleFormat, SubtitleLanguage, SubtitlePattern, SubtitleType
class SubtitleKnowledge(Protocol):
"""Read-only query surface for subtitle knowledge consumed by the domain.
Only the methods that domain services actually call belong here — anything
else (defaults loading, reload, pattern groups, raw dicts) stays on the
concrete class and is reserved for the application layer.
"""
def known_extensions(self) -> set[str]: ...
def format_for_extension(self, ext: str) -> SubtitleFormat | None: ...
def language_for_token(self, token: str) -> SubtitleLanguage | None: ...
def is_known_lang_token(self, token: str) -> bool: ...
def type_for_token(self, token: str) -> SubtitleType | None: ...
def is_known_type_token(self, token: str) -> bool: ...
def patterns(self) -> dict[str, SubtitlePattern]: ...
@@ -4,9 +4,8 @@ import logging
import re import re
from pathlib import Path from pathlib import Path
from alfred.infrastructure.knowledge.subtitles.base import SubtitleKnowledgeBase
from ...shared.ports import FilesystemScanner, MediaProber from ...shared.ports import FilesystemScanner, MediaProber
from ..ports import SubtitleKnowledge
from ...shared.value_objects import ImdbId from ...shared.value_objects import ImdbId
from ..entities import MediaSubtitleMetadata, SubtitleCandidate from ..entities import MediaSubtitleMetadata, SubtitleCandidate
from ..value_objects import ScanStrategy, SubtitlePattern, SubtitleType from ..value_objects import ScanStrategy, SubtitlePattern, SubtitleType
@@ -59,7 +58,7 @@ class SubtitleIdentifier:
def __init__( def __init__(
self, self,
kb: SubtitleKnowledgeBase, kb: SubtitleKnowledge,
prober: MediaProber, prober: MediaProber,
scanner: FilesystemScanner, scanner: FilesystemScanner,
): ):
@@ -3,9 +3,8 @@
import logging import logging
from pathlib import Path from pathlib import Path
from alfred.infrastructure.knowledge.subtitles.base import SubtitleKnowledgeBase
from ...shared.ports import FilesystemScanner, MediaProber from ...shared.ports import FilesystemScanner, MediaProber
from ..ports import SubtitleKnowledge
from ..value_objects import ScanStrategy, SubtitlePattern from ..value_objects import ScanStrategy, SubtitlePattern
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -22,7 +21,7 @@ class PatternDetector:
def __init__( def __init__(
self, self,
kb: SubtitleKnowledgeBase, kb: SubtitleKnowledge,
prober: MediaProber, prober: MediaProber,
scanner: FilesystemScanner, scanner: FilesystemScanner,
): ):