From 16673f4559d605078177b90dc0f4d33f31e89955 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 15 Jun 2026 16:12:54 +0200 Subject: [PATCH] fix(quality-scan): scope to library tracks, skip orphan leftovers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- core/repair_jobs/quality_upgrade_scanner.py | 57 ++++++++++++++++++--- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/core/repair_jobs/quality_upgrade_scanner.py b/core/repair_jobs/quality_upgrade_scanner.py index b6f16435..e095eeac 100644 --- a/core/repair_jobs/quality_upgrade_scanner.py +++ b/core/repair_jobs/quality_upgrade_scanner.py @@ -51,7 +51,10 @@ class QualityUpgradeScannerJob(RepairJob): icon = 'repair-icon-lossless' default_enabled = False 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 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_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 + not_in_library = 0 for i, fpath in enumerate(audio_files): if context.check_stop(): return result if i % 20 == 0 and context.wait_if_paused(): return result - result.scanned += 1 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: context.report_progress( scanned=i + 1, total=total, @@ -145,8 +167,7 @@ class QualityUpgradeScannerJob(RepairJob): context.update_progress(i + 1, total) continue - # Below profile → resolve metadata (DB row preferred, file tags fallback). - meta = self._lookup_meta(fpath, db_index) + # Below profile → build the finding from the resolved metadata. current_label = aq.label() target_labels = [t.label for t in targets] disp_title = meta.get('title') or os.path.splitext(fname)[0] @@ -201,10 +222,26 @@ class QualityUpgradeScannerJob(RepairJob): if probe_failed: logger.warning("[QualityScan] %d/%d files could not be probed (unreadable)", 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", result.scanned, result.findings_created, result.skipped) 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: """All existing music directories to walk, as absolute paths (dedup).""" cm = context.config_manager @@ -273,16 +310,20 @@ class QualityUpgradeScannerJob(RepairJob): conn.close() return index - def _lookup_meta(self, fpath: str, db_index: dict) -> dict: - """Match a walked file to a DB track via path suffix; fall back to the - file's own tags for title/artist/album when nothing matches.""" + def _match_db(self, fpath: str, db_index: dict): + """Match a walked file to a DB track via path suffix. Returns the track + meta dict, or None when the file isn't part of the library.""" parts = fpath.replace('\\', '/').split('/') for depth in range(min(3, len(parts)), 0, -1): suffix = '/'.join(parts[-depth:]).lower() hit = db_index.get(suffix) if 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} try: from mutagen import File as MutagenFile