Fix dismissed findings reappearing and reduce false orphan detections

The finding dedup check only looked for 'pending' and 'resolved' status,
missing 'dismissed'. Dismissed findings were recreated as new entries on
every scan. Now includes 'dismissed' in the dedup check.

Orphan file detector improvements:
- Increased path suffix matching depth from 3 to 4 segments (covers
  Genre/Artist/Album/track.flac paths)
- Added filename-based fallback when Mutagen can't read file tags —
  parses title from "NN - Title [Quality].ext" pattern and matches
  against parent/grandparent folder names as artist
This commit is contained in:
Broque Thomas 2026-04-10 17:42:39 -07:00
parent acb4479313
commit 5be6a46fb0
2 changed files with 33 additions and 5 deletions

View file

@ -61,8 +61,9 @@ class OrphanFileDetectorJob(RepairJob):
cursor.execute("SELECT file_path FROM tracks WHERE file_path IS NOT NULL AND file_path != ''")
for row in cursor.fetchall():
parts = row[0].replace('\\', '/').split('/')
# Store last 1, 2, and 3 path components as lowercase suffixes
for depth in range(1, min(4, len(parts) + 1)):
# Store last 1-4 path components as lowercase suffixes.
# Depth 4 covers Genre/Artist/Album/track.flac scenarios.
for depth in range(1, min(5, len(parts) + 1)):
suffix = '/'.join(parts[-depth:]).lower()
known_suffixes.add(suffix)
@ -127,7 +128,7 @@ class OrphanFileDetectorJob(RepairJob):
# Check if this file matches any known DB path via suffix matching
fpath_parts = fpath.replace('\\', '/').split('/')
is_known = False
for depth in range(1, min(4, len(fpath_parts) + 1)):
for depth in range(1, min(5, len(fpath_parts) + 1)):
suffix = '/'.join(fpath_parts[-depth:]).lower()
if suffix in known_suffixes:
is_known = True
@ -161,6 +162,33 @@ class OrphanFileDetectorJob(RepairJob):
except Exception:
pass
# Last resort: parse title from filename pattern "NN - Title [Quality].ext"
# and match against known titles. Catches files with unreadable tags.
if not is_known and known_titles:
try:
fname_base = os.path.splitext(os.path.basename(fpath))[0]
# Strip quality tags like [FLAC 16bit], [MP3-320]
fname_clean = re.sub(r'\s*\[.*?\]\s*$', '', fname_base).strip()
# Strip leading track number: "01 - Title" → "Title"
fname_clean = re.sub(r'^\d{1,3}\s*[-.]\s*', '', fname_clean).strip()
if fname_clean:
fname_lower = fname_clean.lower()
# Extract artist from parent folder
parent_folder = os.path.basename(os.path.dirname(fpath)).lower().strip()
# Try artist from grandparent (Artist/Album/track.flac)
grandparent = os.path.basename(os.path.dirname(os.path.dirname(fpath))).lower().strip()
for folder_artist in [parent_folder, grandparent]:
if (fname_lower, folder_artist) in known_titles:
is_known = True
break
clean_fn = _strip_extras(fname_lower)
clean_fa = _strip_extras(folder_artist)
if clean_fn and (clean_fn, clean_fa) in known_titles_clean:
is_known = True
break
except Exception:
pass
if not is_known:
orphan_files.append(fpath)

View file

@ -632,11 +632,11 @@ class RepairWorker:
conn = self.db._get_connection()
cursor = conn.cursor()
# Dedup check: skip if same finding already exists (pending OR recently resolved)
# Dedup check: skip if same finding already exists (pending, resolved, OR dismissed)
cursor.execute("""
SELECT id FROM repair_findings
WHERE job_id = ? AND finding_type = ?
AND status IN ('pending', 'resolved')
AND status IN ('pending', 'resolved', 'dismissed')
AND ((entity_type = ? AND entity_id = ?) OR (file_path = ? AND file_path IS NOT NULL))
LIMIT 1
""", (job_id, finding_type, entity_type, entity_id, file_path))