soulsync/core/repair_jobs/__init__.py
BoulderBadgeDad f9271c0cd8 Canonical album version — backfill job (the opt-in activation)
The populate trigger that turns the (until now dormant) feature on. Until a user
enables and runs this job, no album has a canonical -> both read sides (Stages
3-4) fall back -> zero behavior change. So the whole feature ships safely off.

- core/repair_jobs/canonical_version_resolve.py — "Resolve Canonical Album
  Versions". Iterates the active server's albums, skips ones already pinned, and
  calls the tested resolve_and_store_canonical_for_album per album. Opt-in
  (default_enabled=False) and dry-run-by-default: resolving compares an album's
  candidate releases across sources (metadata-source API calls, once per album),
  so it's deliberately user-triggered. Dry run reports a finding per album it
  would pin; live mode stores. Registered in _JOB_MODULES.
- core/metadata/canonical_resolver.py — resolve_and_store gains store=True; the
  job's dry run passes store=False to resolve-without-writing.

Tests: tests/test_canonical_version_job.py (6) — registered, opt-in + dry-run
defaults, live resolves+stores (auto_fixed), dry run creates findings without
persisting, already-pinned albums skipped. Registry loads all 19 jobs cleanly.
145 tests across the full feature + reorganize/track-repair/DB regression pass.
2026-06-02 11:53:45 -07:00

66 lines
2 KiB
Python

"""Repair Jobs Registry — all available maintenance jobs for the Library Worker."""
import importlib
from core.repair_jobs.base import RepairJob, JobContext, JobResult
from utils.logging_config import get_logger
logger = get_logger("repair_jobs")
# Registry populated at import time by each job module
JOB_REGISTRY: dict[str, type[RepairJob]] = {}
_imports_done = False
def register_job(cls: type[RepairJob]) -> type[RepairJob]:
"""Decorator to register a RepairJob subclass."""
JOB_REGISTRY[cls.job_id] = cls
return cls
def get_all_jobs() -> dict[str, type[RepairJob]]:
"""Return the full job registry. Ensures all job modules are imported."""
_import_all_jobs()
return JOB_REGISTRY
_JOB_MODULES = [
'core.repair_jobs.track_number_repair',
'core.repair_jobs.cache_evictor',
'core.repair_jobs.orphan_file_detector',
'core.repair_jobs.dead_file_cleaner',
'core.repair_jobs.duplicate_detector',
'core.repair_jobs.acoustid_scanner',
'core.repair_jobs.missing_cover_art',
'core.repair_jobs.metadata_gap_filler',
'core.repair_jobs.album_completeness',
'core.repair_jobs.fake_lossless_detector',
'core.repair_jobs.library_reorganize',
'core.repair_jobs.mbid_mismatch_detector',
'core.repair_jobs.single_album_dedup',
'core.repair_jobs.lossy_converter',
'core.repair_jobs.album_tag_consistency',
'core.repair_jobs.live_commentary_cleaner',
'core.repair_jobs.unknown_artist_fixer',
'core.repair_jobs.discography_backfill',
'core.repair_jobs.canonical_version_resolve',
]
def _import_all_jobs():
"""Import all job modules to trigger registration.
Each module is imported individually so that a failure in one
does not prevent the others from loading.
"""
global _imports_done
if _imports_done:
return
_imports_done = True
for module_name in _JOB_MODULES:
try:
importlib.import_module(module_name)
except Exception as e:
logger.error("Failed to import job module %s: %s", module_name, e)