Files
alfred/alfred/domain/subtitles/aggregates.py
T
francwa 6e252d1e81 refactor(subtitles): inject default rules into SubtitleRuleSet.resolve()
aggregates.py used to call SubtitleKnowledgeBase().default_rules() via a
DEFAULT_RULES() helper, which silently pulled the infrastructure layer
(YAML loader) into the domain on every resolve.

Make the dependency explicit: resolve() now takes the default rules as
a parameter, and the caller (the ManageSubtitles use case) loads them
from the KB once and passes them in. Domain stays I/O-free.

- Drop DEFAULT_RULES helper and the SubtitleKnowledgeBase import from
  alfred/domain/subtitles/aggregates.py
- SubtitleRuleSet.resolve(default_rules: SubtitleMatchingRules)
- manage_subtitles use case passes kb.default_rules() at the call site
- Tests use a local SubtitleMatchingRules stand-in instead of relying
  on KB defaults
2026-05-19 15:10:06 +02:00

96 lines
3.6 KiB
Python

"""Subtitle domain aggregates."""
from dataclasses import dataclass, field
from typing import Any
from ..shared.value_objects import ImdbId
from .value_objects import RuleScope, SubtitleMatchingRules
@dataclass
class SubtitleRuleSet:
"""
Rules for subtitle selection at a given scope level, with inheritance.
Only delta fields are stored — None means "inherit from parent".
Resolution order: global → release_group → show/movie → season → episode.
A RuleSet can also be pinned to a specific media item (imdb_id),
bypassing the scope hierarchy for that item.
"""
scope: RuleScope
parent: SubtitleRuleSet | None = None
pinned_to: ImdbId | None = None
# Deltas — None = inherit
_languages: list[str] | None = field(default=None, repr=False)
_formats: list[str] | None = field(default=None, repr=False)
_types: list[str] | None = field(default=None, repr=False)
_format_priority: list[str] | None = field(default=None, repr=False)
_min_confidence: float | None = field(default=None, repr=False)
def resolve(self, default_rules: SubtitleMatchingRules) -> SubtitleMatchingRules:
"""
Walk the parent chain and merge deltas into effective rules.
``default_rules`` seeds the top of the chain — it is the caller's
responsibility to load these from the knowledge base (infrastructure).
Keeping the default rules as a parameter keeps the domain free of
any I/O dependency.
"""
base = (
self.parent.resolve(default_rules) if self.parent else default_rules
)
return SubtitleMatchingRules(
preferred_languages=self._languages or base.preferred_languages,
preferred_formats=self._formats or base.preferred_formats,
allowed_types=self._types or base.allowed_types,
format_priority=self._format_priority or base.format_priority,
min_confidence=self._min_confidence
if self._min_confidence is not None
else base.min_confidence,
)
def override(
self,
languages: list[str] | None = None,
formats: list[str] | None = None,
types: list[str] | None = None,
format_priority: list[str] | None = None,
min_confidence: float | None = None,
) -> None:
"""Set delta overrides at this scope level."""
if languages is not None:
self._languages = languages
if formats is not None:
self._formats = formats
if types is not None:
self._types = types
if format_priority is not None:
self._format_priority = format_priority
if min_confidence is not None:
self._min_confidence = min_confidence
def to_dict(self) -> dict:
"""Serialize deltas only (for persistence in rules.yaml)."""
delta: dict[str, Any] = {}
if self._languages is not None:
delta["languages"] = self._languages
if self._formats is not None:
delta["formats"] = self._formats
if self._types is not None:
delta["types"] = self._types
if self._format_priority is not None:
delta["format_priority"] = self._format_priority
if self._min_confidence is not None:
delta["min_confidence"] = self._min_confidence
return {
"scope": {"level": self.scope.level, "identifier": self.scope.identifier},
"override": delta,
}
@classmethod
def global_default(cls) -> SubtitleRuleSet:
return cls(scope=RuleScope(level="global"))