fix(quality-scan): scope to library tracks, skip orphan leftovers
The folder walk found 403 files in transfer/downloads when the user's library is only ~18 tracks — the rest are pre-import leftovers (residue after a DB reset). Those are orphans, not library tracks, and belong to the Orphan File Detector, not a quality scan. - New setting library_tracks_only (default True): match each walked file to a DB track via the suffix index BEFORE probing; skip anything with no DB row. So the scan reflects the real library, not download junk, and avoids probing hundreds of orphan files. - Split _lookup_meta into _match_db (cheap DB suffix match) + _read_file_tags (only used when library_tracks_only is off, for loose files). - Log how many files were skipped as not-in-library. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0e14ff03ee
commit
16673f4559
1 changed files with 49 additions and 8 deletions
|
|
@ -51,7 +51,10 @@ class QualityUpgradeScannerJob(RepairJob):
|
||||||
icon = 'repair-icon-lossless'
|
icon = 'repair-icon-lossless'
|
||||||
default_enabled = False
|
default_enabled = False
|
||||||
default_interval_hours = 168
|
default_interval_hours = 168
|
||||||
default_settings = {}
|
# library_tracks_only: only check files that match a library DB track. OFF
|
||||||
|
# would also flag loose files in transfer/downloads (pre-import leftovers).
|
||||||
|
default_settings = {'library_tracks_only': True}
|
||||||
|
setting_options = {'library_tracks_only': [True, False]}
|
||||||
auto_fix = False # User chooses fix action per finding
|
auto_fix = False # User chooses fix action per finding
|
||||||
|
|
||||||
def scan(self, context: JobContext) -> JobResult:
|
def scan(self, context: JobContext) -> JobResult:
|
||||||
|
|
@ -112,16 +115,35 @@ class QualityUpgradeScannerJob(RepairJob):
|
||||||
|
|
||||||
# --- DB suffix index so a walked file maps back to its track row ---
|
# --- DB suffix index so a walked file maps back to its track row ---
|
||||||
db_index = self._build_db_suffix_index(context)
|
db_index = self._build_db_suffix_index(context)
|
||||||
|
# Only check files that are part of the LIBRARY (have a DB track row).
|
||||||
|
# The transfer/download folders also hold pre-import leftovers (e.g.
|
||||||
|
# residue after a DB reset) — those are orphans, not library tracks, and
|
||||||
|
# belong to the Orphan File Detector, not a quality upgrade scan. Default
|
||||||
|
# ON so the scan reflects the user's actual library, not download junk.
|
||||||
|
library_only = self._get_settings(context).get('library_tracks_only', True)
|
||||||
|
|
||||||
probe_failed = 0
|
probe_failed = 0
|
||||||
|
not_in_library = 0
|
||||||
for i, fpath in enumerate(audio_files):
|
for i, fpath in enumerate(audio_files):
|
||||||
if context.check_stop():
|
if context.check_stop():
|
||||||
return result
|
return result
|
||||||
if i % 20 == 0 and context.wait_if_paused():
|
if i % 20 == 0 and context.wait_if_paused():
|
||||||
return result
|
return result
|
||||||
|
|
||||||
result.scanned += 1
|
|
||||||
fname = os.path.basename(fpath)
|
fname = os.path.basename(fpath)
|
||||||
|
|
||||||
|
# Map to a DB track up front (cheap suffix lookup). When scoping to
|
||||||
|
# the library, skip anything with no DB row BEFORE probing — no point
|
||||||
|
# reading hundreds of orphan files.
|
||||||
|
meta = self._match_db(fpath, db_index)
|
||||||
|
if library_only and meta is None:
|
||||||
|
not_in_library += 1
|
||||||
|
result.skipped += 1
|
||||||
|
continue
|
||||||
|
if meta is None:
|
||||||
|
meta = self._read_file_tags(fpath)
|
||||||
|
|
||||||
|
result.scanned += 1
|
||||||
if context.report_progress and i % 25 == 0:
|
if context.report_progress and i % 25 == 0:
|
||||||
context.report_progress(
|
context.report_progress(
|
||||||
scanned=i + 1, total=total,
|
scanned=i + 1, total=total,
|
||||||
|
|
@ -145,8 +167,7 @@ class QualityUpgradeScannerJob(RepairJob):
|
||||||
context.update_progress(i + 1, total)
|
context.update_progress(i + 1, total)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Below profile → resolve metadata (DB row preferred, file tags fallback).
|
# Below profile → build the finding from the resolved metadata.
|
||||||
meta = self._lookup_meta(fpath, db_index)
|
|
||||||
current_label = aq.label()
|
current_label = aq.label()
|
||||||
target_labels = [t.label for t in targets]
|
target_labels = [t.label for t in targets]
|
||||||
disp_title = meta.get('title') or os.path.splitext(fname)[0]
|
disp_title = meta.get('title') or os.path.splitext(fname)[0]
|
||||||
|
|
@ -201,10 +222,26 @@ class QualityUpgradeScannerJob(RepairJob):
|
||||||
if probe_failed:
|
if probe_failed:
|
||||||
logger.warning("[QualityScan] %d/%d files could not be probed (unreadable)",
|
logger.warning("[QualityScan] %d/%d files could not be probed (unreadable)",
|
||||||
probe_failed, total)
|
probe_failed, total)
|
||||||
|
if not_in_library:
|
||||||
|
logger.info(
|
||||||
|
"[QualityScan] %d/%d files skipped — not in the library DB (orphan "
|
||||||
|
"leftovers in transfer/downloads; disable 'library_tracks_only' to "
|
||||||
|
"include them)", not_in_library, total)
|
||||||
logger.info("Quality upgrade scan: %d checked, %d below profile, %d skipped",
|
logger.info("Quality upgrade scan: %d checked, %d below profile, %d skipped",
|
||||||
result.scanned, result.findings_created, result.skipped)
|
result.scanned, result.findings_created, result.skipped)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _get_settings(self, context: JobContext) -> dict:
|
||||||
|
merged = dict(self.default_settings)
|
||||||
|
if context.config_manager:
|
||||||
|
try:
|
||||||
|
cfg = context.config_manager.get(f'repair.jobs.{self.job_id}.settings', {})
|
||||||
|
if isinstance(cfg, dict):
|
||||||
|
merged.update(cfg)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("settings read failed: %s", e)
|
||||||
|
return merged
|
||||||
|
|
||||||
def _collect_music_dirs(self, context: JobContext) -> list:
|
def _collect_music_dirs(self, context: JobContext) -> list:
|
||||||
"""All existing music directories to walk, as absolute paths (dedup)."""
|
"""All existing music directories to walk, as absolute paths (dedup)."""
|
||||||
cm = context.config_manager
|
cm = context.config_manager
|
||||||
|
|
@ -273,16 +310,20 @@ class QualityUpgradeScannerJob(RepairJob):
|
||||||
conn.close()
|
conn.close()
|
||||||
return index
|
return index
|
||||||
|
|
||||||
def _lookup_meta(self, fpath: str, db_index: dict) -> dict:
|
def _match_db(self, fpath: str, db_index: dict):
|
||||||
"""Match a walked file to a DB track via path suffix; fall back to the
|
"""Match a walked file to a DB track via path suffix. Returns the track
|
||||||
file's own tags for title/artist/album when nothing matches."""
|
meta dict, or None when the file isn't part of the library."""
|
||||||
parts = fpath.replace('\\', '/').split('/')
|
parts = fpath.replace('\\', '/').split('/')
|
||||||
for depth in range(min(3, len(parts)), 0, -1):
|
for depth in range(min(3, len(parts)), 0, -1):
|
||||||
suffix = '/'.join(parts[-depth:]).lower()
|
suffix = '/'.join(parts[-depth:]).lower()
|
||||||
hit = db_index.get(suffix)
|
hit = db_index.get(suffix)
|
||||||
if hit:
|
if hit:
|
||||||
return hit
|
return hit
|
||||||
# No DB row — read the file's own tags.
|
return None
|
||||||
|
|
||||||
|
def _read_file_tags(self, fpath: str) -> dict:
|
||||||
|
"""Read title/artist/album from the file's own tags (for loose files
|
||||||
|
when library_tracks_only is off)."""
|
||||||
meta = {'track_id': None}
|
meta = {'track_id': None}
|
||||||
try:
|
try:
|
||||||
from mutagen import File as MutagenFile
|
from mutagen import File as MutagenFile
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue