Replaces the wishlist-only Quality Scanner with a proper Library Maintenance
job that produces actionable findings — same model as the AcoustID/orphan
tools, per user request ("mach ein finding wie jedes anderes Tool").
- New core/repair_jobs/quality_upgrade_scanner.py: iterates DB library
tracks, resolves each path via the shared resolver (now index-0 correct for
relative library paths), probes REAL audio quality with the same
probe_audio_quality the download import guard uses, and checks it against the
user's v3 ranked targets via quality_meets_profile (strict — no extension
guessing, no fallback). Below-profile tracks become 'quality_upgrade'
findings with current vs target quality in details.
- repair_worker._fix_quality_upgrade: redownload (wishlist + delete file/row),
delete (file + row), or ignore (dismiss in UI). Registered in _execute_fix
dispatch + bulk fixable_types.
- Frontend (enrichment.js): 'Low Quality' type label, 'Upgrade' fix button, a
3-way _promptQualityUpgradeAction modal (Re-download / Delete / Ignore),
wired into both single-finding fix and bulk-fix (Ignore → dismiss inline).
- Tools "Quality Scanner" button now triggers Run Now of this job and points
the user to Library Maintenance → Findings.
The old standalone /api/quality-scanner endpoints are left intact (unused by the
button) to avoid churn. Verified: job registers, fix handler dispatches.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
72 lines
2.3 KiB
Python
72 lines
2.3 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.replaygain_filler',
|
|
'core.repair_jobs.empty_folder_cleaner',
|
|
'core.repair_jobs.expired_download_cleaner',
|
|
'core.repair_jobs.metadata_gap_filler',
|
|
'core.repair_jobs.album_completeness',
|
|
'core.repair_jobs.fake_lossless_detector',
|
|
'core.repair_jobs.quality_upgrade_scanner',
|
|
'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)
|