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.
66 lines
2 KiB
Python
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)
|