HiFi (and occasionally other) downloads sometimes deliver a ~30s preview clip instead of the full song; it lands in the library looking real. new repair job scans short tracks (duration <= 30s, configurable), looks up the EXPECTED length from the track's metadata source (spotify/itunes/mb get_track_details), and flags any whose real length is much longer than the file (default: >= 30s longer) as a preview clip. approving the finding (repair_worker._fix_short_preview_track) deletes the preview file (path resolved via _resolve_file_path like the other delete tools), drops the DB row so the track goes missing, and re-adds it to the wishlist with the full payload (mirrors _fix_dead_file) so the real version downloads. scan ONLY creates findings — nothing destructive without user approval, like every other tool. conservative: genuine short tracks (source agrees they're short) and tracks whose length can't be verified are skipped, never flagged. registered the job + finding-type label/fix-button in the UI. 5 tests (scan flag/skip/scope + fix delete+remove+wishlist); 89 repair tests green.
74 lines
2.4 KiB
Python
74 lines
2.4 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',
|
|
'core.repair_jobs.quality_upgrade',
|
|
'core.repair_jobs.short_preview_track',
|
|
]
|
|
|
|
|
|
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)
|