Fix false album completion badges, add multi-artist album matching
Completion accuracy: - Exact match only: "Complete" requires owned_tracks >= expected_tracks, no more 90% rounding that hid missing tracks - Deduplicate track counting: DISTINCT (title, track_number) prevents duplicate album entries from inflating owned count (e.g., 3 "GNX" entries with 12+1+2 rows counted as 12 unique tracks, not 15) - MAX(track_count) instead of SUM for stored count — uses largest album entry rather than summing duplicates - file_path IS NOT NULL filter ensures only real files are counted - Frontend uses real numbers instead of overriding missing=0 when backend says "completed" Multi-artist albums: - Title-only fallback search when artist-specific search fails - Finds "Anger Management" filed under "The Alchemist" when checking from Rico Nasty's page - Same confidence scoring prevents false matches
This commit is contained in:
parent
a33f891fa6
commit
a0b2fa9441
3 changed files with 43 additions and 24 deletions
|
|
@ -4991,12 +4991,19 @@ class MusicDatabase:
|
|||
sibling_ids = [row['id'] for row in cursor.fetchall()]
|
||||
|
||||
# Get actual track count across all sibling album entries
|
||||
# Count DISTINCT titles to deduplicate across split/duplicate album entries
|
||||
# (e.g., 3 "GNX" albums with 12+1+2 tracks = 15 rows but only 12 unique songs)
|
||||
placeholders = ','.join('?' for _ in sibling_ids)
|
||||
cursor.execute(f"SELECT COUNT(*) FROM tracks WHERE album_id IN ({placeholders})", sibling_ids)
|
||||
cursor.execute(f"""
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT DISTINCT LOWER(title), track_number FROM tracks
|
||||
WHERE album_id IN ({placeholders}) AND file_path IS NOT NULL AND file_path != ''
|
||||
)
|
||||
""", sibling_ids)
|
||||
owned_tracks = cursor.fetchone()[0]
|
||||
|
||||
# Get combined expected track count from all sibling album entries
|
||||
cursor.execute(f"SELECT SUM(track_count) FROM albums WHERE id IN ({placeholders})", sibling_ids)
|
||||
# Get the max track_count from sibling albums (not SUM — avoids inflating from duplicates)
|
||||
cursor.execute(f"SELECT MAX(track_count) FROM albums WHERE id IN ({placeholders})", sibling_ids)
|
||||
result = cursor.fetchone()
|
||||
stored_track_count = result[0] if result and result[0] else 0
|
||||
|
||||
|
|
@ -5008,9 +5015,7 @@ class MusicDatabase:
|
|||
if (expected_track_count is not None and stored_track_count > 0
|
||||
and owned_tracks >= stored_track_count
|
||||
and stored_track_count >= expected_track_count * 0.6):
|
||||
# Album is complete by its own metadata — don't inflate expected with a different edition's count.
|
||||
# Guard: stored count must be >=60% of expected to look like a plausible edition variant
|
||||
# (standard 12 vs deluxe 20 = 60%), not just Plex's leafCount reflecting partial ownership.
|
||||
# Album is complete by its own metadata — standard vs deluxe edition difference
|
||||
expected_tracks = stored_track_count
|
||||
elif expected_track_count is not None:
|
||||
expected_tracks = expected_track_count
|
||||
|
|
@ -5019,11 +5024,10 @@ class MusicDatabase:
|
|||
|
||||
# Determine completeness with refined thresholds
|
||||
if expected_tracks and expected_tracks > 0:
|
||||
completion_ratio = owned_tracks / expected_tracks
|
||||
# Complete: 90%+, Nearly Complete: 80-89%, Partial: <80%
|
||||
is_complete = completion_ratio >= 0.9 and owned_tracks > 0
|
||||
# Exact match — complete only when owned == expected
|
||||
is_complete = owned_tracks >= expected_tracks
|
||||
else:
|
||||
# Fallback: if we have any tracks, consider it owned
|
||||
# No expected count known — complete if we have any tracks
|
||||
is_complete = owned_tracks > 0
|
||||
|
||||
# Get distinct format strings for owned tracks
|
||||
|
|
@ -5167,6 +5171,26 @@ class MusicDatabase:
|
|||
logger.debug(f"✅ Fallback match succeeded: '{title}' -> '{best_match.title}' (confidence: {best_confidence:.3f})")
|
||||
return best_match, best_confidence
|
||||
|
||||
# Multi-artist fallback: search by title only (any artist)
|
||||
# Handles collaborative albums filed under a different artist in the library
|
||||
if best_confidence < confidence_threshold:
|
||||
logger.debug(f"⚠️ Artist-specific search failed, trying title-only fallback for '{title}'")
|
||||
try:
|
||||
title_only_albums = self.search_albums(title=title, artist="", limit=20, server_source=server_source)
|
||||
for album in title_only_albums:
|
||||
confidence = self._calculate_album_confidence(title, artist, album, expected_track_count)
|
||||
# Slightly penalize cross-artist matches to prefer same-artist when possible
|
||||
if confidence > best_confidence:
|
||||
best_confidence = confidence
|
||||
best_match = album
|
||||
logger.debug(f" 🎯 Title-only match: '{album.title}' (confidence: {confidence:.3f})")
|
||||
except Exception as title_error:
|
||||
logger.warning(f"Title-only fallback search failed: {title_error}")
|
||||
|
||||
if best_match and best_confidence >= confidence_threshold:
|
||||
logger.debug(f"✅ Title-only match succeeded: '{title}' -> '{best_match.title}' (confidence: {best_confidence:.3f})")
|
||||
return best_match, best_confidence
|
||||
|
||||
logger.debug(f"❌ No confident edition match for '{title}' (best: {best_confidence:.3f}, threshold: {confidence_threshold})")
|
||||
return None, best_confidence
|
||||
|
||||
|
|
|
|||
|
|
@ -9736,12 +9736,10 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b
|
|||
else:
|
||||
completion_percentage = 100 if owned_tracks > 0 else 0
|
||||
|
||||
# Determine completion status based on percentage
|
||||
if completion_percentage >= 90 and owned_tracks > 0:
|
||||
# Determine completion status — exact match, no percentage rounding
|
||||
if owned_tracks > 0 and owned_tracks >= (expected_tracks or total_tracks):
|
||||
status = "completed"
|
||||
elif completion_percentage >= 60:
|
||||
status = "nearly_complete"
|
||||
elif completion_percentage > 0:
|
||||
elif owned_tracks > 0:
|
||||
status = "partial"
|
||||
else:
|
||||
status = "missing"
|
||||
|
|
@ -9820,12 +9818,10 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode:
|
|||
else:
|
||||
completion_percentage = (owned_tracks / total_tracks) * 100
|
||||
|
||||
# Determine status
|
||||
if completion_percentage >= 90 and owned_tracks > 0:
|
||||
# Determine status — exact match, no percentage rounding
|
||||
if owned_tracks > 0 and owned_tracks >= (expected_tracks or total_tracks):
|
||||
status = "completed"
|
||||
elif completion_percentage >= 60:
|
||||
status = "nearly_complete"
|
||||
elif completion_percentage > 0:
|
||||
elif owned_tracks > 0:
|
||||
status = "partial"
|
||||
else:
|
||||
status = "missing"
|
||||
|
|
|
|||
|
|
@ -40110,10 +40110,9 @@ function updateLibraryReleaseCard(data) {
|
|||
card.classList.add('missing');
|
||||
}
|
||||
|
||||
// If backend says "completed" (>=90%), trust it — Spotify metadata track counts
|
||||
// can be wrong (e.g. total_tracks=15 but API only returns 14 actual tracks)
|
||||
const isComplete = data.status === 'completed';
|
||||
const effectiveMissing = isComplete ? 0 : (data.expected_tracks - data.owned_tracks);
|
||||
// Use real numbers — no rounding or overrides
|
||||
const isComplete = data.owned_tracks >= data.expected_tracks && data.owned_tracks > 0;
|
||||
const effectiveMissing = data.expected_tracks - data.owned_tracks;
|
||||
|
||||
// Update the mutable release data on the card
|
||||
if (card._releaseData) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue