soulsync/core/repair_jobs/__init__.py
BoulderBadgeDad 1051ef2402 Lyrics: add a "Lyrics Filler" maintenance job + lyrics option in the Re-tag tool (Sokhi)
The lyrics sibling of the Cover Art Filler, plus retag integration — reusing
the existing LyricsClient (LRClib) the import pipeline already uses.

- lyrics_client: extracted the LRClib fetch (exact-match-with-duration →
  search fallback) into a shared _fetch_remote_lyrics, used by both
  create_lrc_file (unchanged behavior) and a new check-only has_remote_lyrics.
- MissingLyricsJob (core/repair_jobs/missing_lyrics.py): scans tracks with no
  .lrc sidecar and — Option A — only flags ones LRClib actually has lyrics
  for, so instrumentals/interludes are never surfaced or re-flagged. Registered
  in the job list; default OFF; respects the lrclib_enabled toggle.
- _fix_missing_lyrics (repair_worker): applies a finding by fetching + writing
  the .lrc and embedding lyrics via create_lrc_file.
- Re-tag tool: new 'lyrics' setting ('fetch'|'skip', default skip). When
  'fetch', apply_track_plans now also fetches/refreshes the .lrc per track
  (fetch-if-missing, re-embed-if-exists) — threaded through scan gates, finding
  details, the auto-apply path, and the manual fix handler. Settings UI
  auto-renders the dropdown from setting_options; no markup needed.
- Frontend: type/action/result labels for missing_lyrics + a finding detail
  render case.

Tests: 12 — has_remote_lyrics truth table, sidecar detection, scan (only-
fixable / skip-existing / lrclib-disabled), the apply handler, and retag
lyrics_action on/off. 694 repair/lyrics/cover/retag tests pass.
2026-06-07 17:27:52 -07:00

68 lines
2.1 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.missing_lyrics',
'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',
'core.repair_jobs.library_retag',
]
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)