"""RuleSetRepository — loads SubtitleRuleSet from .alfred/ YAML files.""" import logging from pathlib import Path from typing import TYPE_CHECKING import yaml from alfred.domain.subtitles.aggregates import SubtitleRuleSet from alfred.domain.subtitles.value_objects import RuleScope if TYPE_CHECKING: from alfred.infrastructure.persistence.memory.ltm.components.subtitle_preferences import ( SubtitlePreferences, ) logger = logging.getLogger(__name__) def _load_yaml(path: Path) -> dict: if not path.exists(): return {} try: with open(path, encoding="utf-8") as f: return yaml.safe_load(f) or {} except Exception as e: logger.warning(f"RuleSetRepository: could not read {path}: {e}") return {} class RuleSetRepository: """ Builds a fully chained SubtitleRuleSet by reading YAML from .alfred/. Inheritance chain: global (hardcoded defaults) └── release_group (.alfred/release_groups/{GROUP}.yaml) └── local (.alfred/rules.yaml) Rules are delta-only — None means "inherit from parent". The repository only creates intermediate nodes when the corresponding file exists and contains an override section. """ def __init__(self, library_root: Path): self._alfred_dir = library_root / ".alfred" def load( self, release_group: str | None = None, subtitle_preferences: SubtitlePreferences | None = None, ) -> SubtitleRuleSet: """ Build and return the resolved RuleSet chain. If subtitle_preferences is provided, it seeds the global base rule set from LTM (overriding the hardcoded DEFAULT_RULES). Returns global default if no overrides exist. """ base = SubtitleRuleSet.global_default() if subtitle_preferences is not None: base.override( languages=subtitle_preferences.languages, formats=subtitle_preferences.formats, types=subtitle_preferences.types, ) current = base # Release group level if release_group: rg_path = self._alfred_dir / "release_groups" / f"{release_group}.yaml" rg_data = _load_yaml(rg_path).get("override", {}) if rg_data: rg_ruleset = SubtitleRuleSet( scope=RuleScope(level="release_group", identifier=release_group), parent=current, ) rg_ruleset.override(**_filter_override(rg_data)) current = rg_ruleset logger.debug( f"RuleSetRepository: loaded release_group override for '{release_group}'" ) # Local (show/movie) level local_data = _load_yaml(self._alfred_dir / "rules.yaml").get("override", {}) if local_data: local_ruleset = SubtitleRuleSet( scope=RuleScope(level="show"), parent=current, ) local_ruleset.override(**_filter_override(local_data)) current = local_ruleset logger.debug("RuleSetRepository: loaded local rules.yaml override") return current def save_local(self, delta: dict) -> None: """Write or update .alfred/rules.yaml with override delta.""" self._alfred_dir.mkdir(parents=True, exist_ok=True) path = self._alfred_dir / "rules.yaml" existing = _load_yaml(path) existing_override = existing.get("override", {}) existing_override.update(delta) data = {"override": existing_override} tmp = path.with_suffix(".yaml.tmp") try: with open(tmp, "w", encoding="utf-8") as f: yaml.safe_dump( data, f, allow_unicode=True, default_flow_style=False, sort_keys=False, ) tmp.rename(path) logger.info(f"RuleSetRepository: saved local rules to {path}") except Exception as e: logger.error(f"RuleSetRepository: could not write {path}: {e}") tmp.unlink(missing_ok=True) raise def _filter_override(data: dict) -> dict: """Keep only keys that SubtitleRuleSet.override() accepts.""" valid = {"languages", "formats", "types", "format_priority", "min_confidence"} return {k: v for k, v in data.items() if k in valid}