better matching on albums
This commit is contained in:
parent
78d037cba4
commit
019253087f
6 changed files with 447 additions and 123 deletions
Binary file not shown.
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional, Dict, Any, Tuple
|
from typing import List, Optional, Dict, Any, Tuple
|
||||||
|
|
@ -11,6 +13,8 @@ from pathlib import Path
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger("music_database")
|
logger = get_logger("music_database")
|
||||||
|
# Temporarily enable debug logging for edition matching
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DatabaseArtist:
|
class DatabaseArtist:
|
||||||
|
|
@ -578,38 +582,46 @@ class MusicDatabase:
|
||||||
|
|
||||||
def check_track_exists(self, title: str, artist: str, confidence_threshold: float = 0.8) -> Tuple[Optional[DatabaseTrack], float]:
|
def check_track_exists(self, title: str, artist: str, confidence_threshold: float = 0.8) -> Tuple[Optional[DatabaseTrack], float]:
|
||||||
"""
|
"""
|
||||||
Check if a track exists in the database with fuzzy matching and confidence scoring.
|
Check if a track exists in the database with enhanced fuzzy matching and confidence scoring.
|
||||||
|
Now uses the same sophisticated matching approach as album checking for consistency.
|
||||||
Returns (track, confidence) tuple where confidence is 0.0-1.0
|
Returns (track, confidence) tuple where confidence is 0.0-1.0
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Search for potential matches
|
# Generate title variations for better matching (similar to album approach)
|
||||||
potential_matches = self.search_tracks(title=title, artist=artist, limit=20)
|
title_variations = self._generate_track_title_variations(title)
|
||||||
|
|
||||||
if not potential_matches:
|
logger.debug(f"🔍 Enhanced track matching for '{title}' by '{artist}': trying {len(title_variations)} variations")
|
||||||
return None, 0.0
|
for i, var in enumerate(title_variations):
|
||||||
|
logger.debug(f" {i+1}. '{var}'")
|
||||||
|
|
||||||
# Simple confidence scoring based on string similarity
|
|
||||||
def calculate_confidence(db_track: DatabaseTrack) -> float:
|
|
||||||
title_similarity = self._string_similarity(title.lower().strip(), db_track.title.lower().strip())
|
|
||||||
artist_similarity = self._string_similarity(artist.lower().strip(), db_track.artist_name.lower().strip())
|
|
||||||
|
|
||||||
# Weight title slightly more than artist
|
|
||||||
return (title_similarity * 0.6) + (artist_similarity * 0.4)
|
|
||||||
|
|
||||||
# Find best match
|
|
||||||
best_match = None
|
best_match = None
|
||||||
best_confidence = 0.0
|
best_confidence = 0.0
|
||||||
|
|
||||||
for track in potential_matches:
|
# Try each title variation
|
||||||
confidence = calculate_confidence(track)
|
for title_variation in title_variations:
|
||||||
if confidence > best_confidence:
|
# Search for potential matches with this variation
|
||||||
best_confidence = confidence
|
potential_matches = self.search_tracks(title=title_variation, artist=artist, limit=20)
|
||||||
best_match = track
|
|
||||||
|
if not potential_matches:
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.debug(f"🎵 Found {len(potential_matches)} tracks for variation '{title_variation}'")
|
||||||
|
|
||||||
|
# Score each potential match
|
||||||
|
for track in potential_matches:
|
||||||
|
confidence = self._calculate_track_confidence(title, artist, track)
|
||||||
|
logger.debug(f" 🎯 '{track.title}' confidence: {confidence:.3f}")
|
||||||
|
|
||||||
|
if confidence > best_confidence:
|
||||||
|
best_confidence = confidence
|
||||||
|
best_match = track
|
||||||
|
|
||||||
# Return match only if it meets threshold
|
# Return match only if it meets threshold
|
||||||
if best_confidence >= confidence_threshold:
|
if best_match and best_confidence >= confidence_threshold:
|
||||||
|
logger.debug(f"✅ Enhanced track match found: '{title}' -> '{best_match.title}' (confidence: {best_confidence:.3f})")
|
||||||
return best_match, best_confidence
|
return best_match, best_confidence
|
||||||
else:
|
else:
|
||||||
|
logger.debug(f"❌ No confident track match for '{title}' (best: {best_confidence:.3f}, threshold: {confidence_threshold})")
|
||||||
return None, best_confidence
|
return None, best_confidence
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -736,11 +748,12 @@ class MusicDatabase:
|
||||||
def check_album_exists_with_completeness(self, title: str, artist: str, expected_track_count: Optional[int] = None, confidence_threshold: float = 0.8) -> Tuple[Optional[DatabaseAlbum], float, int, int, bool]:
|
def check_album_exists_with_completeness(self, title: str, artist: str, expected_track_count: Optional[int] = None, confidence_threshold: float = 0.8) -> Tuple[Optional[DatabaseAlbum], float, int, int, bool]:
|
||||||
"""
|
"""
|
||||||
Check if an album exists in the database with completeness information.
|
Check if an album exists in the database with completeness information.
|
||||||
|
Enhanced to handle edition matching (standard <-> deluxe variants).
|
||||||
Returns (album, confidence, owned_tracks, expected_tracks, is_complete)
|
Returns (album, confidence, owned_tracks, expected_tracks, is_complete)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# First find the album match
|
# Try enhanced edition-aware matching first with expected track count for Smart Edition Matching
|
||||||
album, confidence = self.check_album_exists(title, artist, confidence_threshold)
|
album, confidence = self.check_album_exists_with_editions(title, artist, confidence_threshold, expected_track_count)
|
||||||
|
|
||||||
if not album:
|
if not album:
|
||||||
return None, 0.0, 0, 0, False
|
return None, 0.0, 0, 0, False
|
||||||
|
|
@ -754,6 +767,301 @@ class MusicDatabase:
|
||||||
logger.error(f"Error checking album existence with completeness for '{title}' by '{artist}': {e}")
|
logger.error(f"Error checking album existence with completeness for '{title}' by '{artist}': {e}")
|
||||||
return None, 0.0, 0, 0, False
|
return None, 0.0, 0, 0, False
|
||||||
|
|
||||||
|
def check_album_exists_with_editions(self, title: str, artist: str, confidence_threshold: float = 0.8, expected_track_count: Optional[int] = None) -> Tuple[Optional[DatabaseAlbum], float]:
|
||||||
|
"""
|
||||||
|
Enhanced album existence check that handles edition variants.
|
||||||
|
Matches standard albums with deluxe/platinum/special editions and vice versa.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Generate album title variations for edition matching
|
||||||
|
title_variations = self._generate_album_title_variations(title)
|
||||||
|
|
||||||
|
logger.debug(f"🔍 Edition matching for '{title}' by '{artist}': trying {len(title_variations)} variations")
|
||||||
|
for i, var in enumerate(title_variations):
|
||||||
|
logger.debug(f" {i+1}. '{var}'")
|
||||||
|
|
||||||
|
best_match = None
|
||||||
|
best_confidence = 0.0
|
||||||
|
|
||||||
|
for variation in title_variations:
|
||||||
|
# Search for this variation
|
||||||
|
albums = self.search_albums(title=variation, artist=artist, limit=10)
|
||||||
|
|
||||||
|
if albums:
|
||||||
|
logger.debug(f"📀 Found {len(albums)} albums for variation '{variation}'")
|
||||||
|
|
||||||
|
if not albums:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Score each potential match with Smart Edition Matching
|
||||||
|
for album in albums:
|
||||||
|
confidence = self._calculate_album_confidence(title, artist, album, expected_track_count)
|
||||||
|
logger.debug(f" 🎯 '{album.title}' confidence: {confidence:.3f}")
|
||||||
|
|
||||||
|
if confidence > best_confidence:
|
||||||
|
best_confidence = confidence
|
||||||
|
best_match = album
|
||||||
|
|
||||||
|
# Return match only if it meets threshold
|
||||||
|
if best_match and best_confidence >= confidence_threshold:
|
||||||
|
logger.debug(f"✅ Edition match found: '{title}' -> '{best_match.title}' (confidence: {best_confidence:.3f})")
|
||||||
|
return best_match, best_confidence
|
||||||
|
else:
|
||||||
|
logger.debug(f"❌ No confident edition match for '{title}' (best: {best_confidence:.3f}, threshold: {confidence_threshold})")
|
||||||
|
return None, best_confidence
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in edition-aware album matching for '{title}' by '{artist}': {e}")
|
||||||
|
return None, 0.0
|
||||||
|
|
||||||
|
def _generate_album_title_variations(self, title: str) -> List[str]:
|
||||||
|
"""Generate variations of album title to handle edition matching"""
|
||||||
|
variations = [title] # Always include original
|
||||||
|
|
||||||
|
# Clean up the title
|
||||||
|
title_lower = title.lower().strip()
|
||||||
|
|
||||||
|
# Define edition patterns and their variations
|
||||||
|
edition_patterns = {
|
||||||
|
r'\s*\(deluxe\s*edition?\)': ['deluxe', 'deluxe edition'],
|
||||||
|
r'\s*\(expanded\s*edition?\)': ['expanded', 'expanded edition'],
|
||||||
|
r'\s*\(platinum\s*edition?\)': ['platinum', 'platinum edition'],
|
||||||
|
r'\s*\(special\s*edition?\)': ['special', 'special edition'],
|
||||||
|
r'\s*\(remastered?\)': ['remastered', 'remaster'],
|
||||||
|
r'\s*\(anniversary\s*edition?\)': ['anniversary', 'anniversary edition'],
|
||||||
|
r'\s*\(.*version\)': ['version'],
|
||||||
|
r'\s+deluxe\s*edition?$': ['deluxe', 'deluxe edition'],
|
||||||
|
r'\s+platinum\s*edition?$': ['platinum', 'platinum edition'],
|
||||||
|
r'\s+special\s*edition?$': ['special', 'special edition'],
|
||||||
|
r'\s*-\s*deluxe': ['deluxe'],
|
||||||
|
r'\s*-\s*platinum\s*edition?': ['platinum', 'platinum edition'],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if title contains any edition indicators
|
||||||
|
base_title = title
|
||||||
|
found_editions = []
|
||||||
|
|
||||||
|
for pattern, edition_types in edition_patterns.items():
|
||||||
|
if re.search(pattern, title_lower):
|
||||||
|
# Remove the edition part to get base title
|
||||||
|
base_title = re.sub(pattern, '', title, flags=re.IGNORECASE).strip()
|
||||||
|
found_editions.extend(edition_types)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Add base title (without edition markers)
|
||||||
|
if base_title != title:
|
||||||
|
variations.append(base_title)
|
||||||
|
|
||||||
|
# If we found a base title, add common edition variants
|
||||||
|
if base_title != title:
|
||||||
|
# Add common deluxe/platinum/special variants
|
||||||
|
common_editions = [
|
||||||
|
'deluxe edition',
|
||||||
|
'deluxe',
|
||||||
|
'platinum edition',
|
||||||
|
'platinum',
|
||||||
|
'special edition',
|
||||||
|
'expanded edition',
|
||||||
|
'remastered',
|
||||||
|
'anniversary edition'
|
||||||
|
]
|
||||||
|
|
||||||
|
for edition in common_editions:
|
||||||
|
variations.extend([
|
||||||
|
f"{base_title} ({edition.title()})",
|
||||||
|
f"{base_title} ({edition})",
|
||||||
|
f"{base_title} - {edition.title()}",
|
||||||
|
f"{base_title} {edition.title()}",
|
||||||
|
])
|
||||||
|
|
||||||
|
# If original title is base form, add edition variants
|
||||||
|
elif not any(re.search(pattern, title_lower) for pattern in edition_patterns.keys()):
|
||||||
|
# This appears to be a base album, add deluxe variants
|
||||||
|
common_editions = ['Deluxe Edition', 'Deluxe', 'Platinum Edition', 'Special Edition']
|
||||||
|
for edition in common_editions:
|
||||||
|
variations.extend([
|
||||||
|
f"{title} ({edition})",
|
||||||
|
f"{title} - {edition}",
|
||||||
|
f"{title} {edition}",
|
||||||
|
])
|
||||||
|
|
||||||
|
# Remove duplicates while preserving order
|
||||||
|
seen = set()
|
||||||
|
unique_variations = []
|
||||||
|
for var in variations:
|
||||||
|
var_clean = var.strip()
|
||||||
|
if var_clean and var_clean.lower() not in seen:
|
||||||
|
seen.add(var_clean.lower())
|
||||||
|
unique_variations.append(var_clean)
|
||||||
|
|
||||||
|
return unique_variations
|
||||||
|
|
||||||
|
def _calculate_album_confidence(self, search_title: str, search_artist: str, db_album: DatabaseAlbum, expected_track_count: Optional[int] = None) -> float:
|
||||||
|
"""Calculate confidence score for album match with Smart Edition Matching"""
|
||||||
|
try:
|
||||||
|
# Simple confidence based on string similarity
|
||||||
|
title_similarity = self._string_similarity(search_title.lower(), db_album.title.lower())
|
||||||
|
artist_similarity = self._string_similarity(search_artist.lower(), db_album.artist_name.lower())
|
||||||
|
|
||||||
|
# Also try with cleaned versions (removing edition markers)
|
||||||
|
clean_search_title = self._clean_album_title_for_comparison(search_title)
|
||||||
|
clean_db_title = self._clean_album_title_for_comparison(db_album.title)
|
||||||
|
clean_title_similarity = self._string_similarity(clean_search_title, clean_db_title)
|
||||||
|
|
||||||
|
# Use the best title similarity
|
||||||
|
best_title_similarity = max(title_similarity, clean_title_similarity)
|
||||||
|
|
||||||
|
# Weight: 50% title, 50% artist (equal weight to prevent false positives)
|
||||||
|
# Also require minimum artist similarity to prevent matching wrong artists
|
||||||
|
confidence = (best_title_similarity * 0.5) + (artist_similarity * 0.5)
|
||||||
|
|
||||||
|
# Apply artist similarity penalty: if artist match is too low, drastically reduce confidence
|
||||||
|
if artist_similarity < 0.6: # Less than 60% artist match
|
||||||
|
confidence *= 0.3 # Reduce confidence by 70%
|
||||||
|
|
||||||
|
# Smart Edition Matching: Boost confidence if we found a "better" edition
|
||||||
|
if expected_track_count and db_album.track_count and clean_title_similarity >= 0.8:
|
||||||
|
# If the cleaned titles match well, check if this is an edition upgrade
|
||||||
|
if db_album.track_count >= expected_track_count:
|
||||||
|
# Found same/better edition (e.g., Deluxe when searching for Standard)
|
||||||
|
edition_bonus = min(0.15, (db_album.track_count - expected_track_count) / expected_track_count * 0.1)
|
||||||
|
confidence += edition_bonus
|
||||||
|
logger.debug(f" 📀 Edition upgrade bonus: +{edition_bonus:.3f} ({db_album.track_count} >= {expected_track_count} tracks)")
|
||||||
|
elif db_album.track_count < expected_track_count * 0.8:
|
||||||
|
# Found significantly smaller edition, apply penalty
|
||||||
|
edition_penalty = 0.1
|
||||||
|
confidence -= edition_penalty
|
||||||
|
logger.debug(f" 📀 Edition downgrade penalty: -{edition_penalty:.3f} ({db_album.track_count} << {expected_track_count} tracks)")
|
||||||
|
|
||||||
|
return min(confidence, 1.0) # Cap at 1.0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error calculating album confidence: {e}")
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def _generate_track_title_variations(self, title: str) -> List[str]:
|
||||||
|
"""Generate variations of track title for better matching"""
|
||||||
|
variations = [title] # Always include original
|
||||||
|
|
||||||
|
# Clean up the title
|
||||||
|
title_lower = title.lower().strip()
|
||||||
|
|
||||||
|
# Common track title variations
|
||||||
|
track_patterns = [
|
||||||
|
# Remove version/remix info
|
||||||
|
r'\s*\(.*version\)',
|
||||||
|
r'\s*\(.*remix\)',
|
||||||
|
r'\s*\(.*mix\)',
|
||||||
|
r'\s*\(.*edit\)',
|
||||||
|
r'\s*\(.*radio\)',
|
||||||
|
# Remove featuring artists
|
||||||
|
r'\s*\(.*feat\..*\)',
|
||||||
|
r'\s*\(.*featuring.*\)',
|
||||||
|
r'\s*\(.*ft\..*\)',
|
||||||
|
# Remove brackets/parentheses content
|
||||||
|
r'\s*\[.*\]',
|
||||||
|
r'\s*\(.*\)',
|
||||||
|
# Remove everything after dash
|
||||||
|
r'\s*-\s*.*'
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in track_patterns:
|
||||||
|
# Apply pattern to original title
|
||||||
|
cleaned = re.sub(pattern, '', title, flags=re.IGNORECASE).strip()
|
||||||
|
if cleaned and cleaned.lower() != title_lower and cleaned not in variations:
|
||||||
|
variations.append(cleaned)
|
||||||
|
|
||||||
|
# Apply pattern to lowercase version
|
||||||
|
cleaned_lower = re.sub(pattern, '', title_lower, flags=re.IGNORECASE).strip()
|
||||||
|
if cleaned_lower and cleaned_lower != title_lower:
|
||||||
|
# Convert back to proper case
|
||||||
|
cleaned_proper = cleaned_lower.title()
|
||||||
|
if cleaned_proper not in variations:
|
||||||
|
variations.append(cleaned_proper)
|
||||||
|
|
||||||
|
# Remove duplicates while preserving order
|
||||||
|
seen = set()
|
||||||
|
unique_variations = []
|
||||||
|
for var in variations:
|
||||||
|
var_key = var.lower().strip()
|
||||||
|
if var_key not in seen and var.strip():
|
||||||
|
seen.add(var_key)
|
||||||
|
unique_variations.append(var.strip())
|
||||||
|
|
||||||
|
return unique_variations
|
||||||
|
|
||||||
|
def _calculate_track_confidence(self, search_title: str, search_artist: str, db_track: DatabaseTrack) -> float:
|
||||||
|
"""Calculate confidence score for track match with enhanced cleaning"""
|
||||||
|
try:
|
||||||
|
# Direct similarity
|
||||||
|
title_similarity = self._string_similarity(search_title.lower(), db_track.title.lower())
|
||||||
|
artist_similarity = self._string_similarity(search_artist.lower(), db_track.artist_name.lower())
|
||||||
|
|
||||||
|
# Also try with cleaned versions (removing parentheses, brackets, etc.)
|
||||||
|
clean_search_title = self._clean_track_title_for_comparison(search_title)
|
||||||
|
clean_db_title = self._clean_track_title_for_comparison(db_track.title)
|
||||||
|
clean_title_similarity = self._string_similarity(clean_search_title, clean_db_title)
|
||||||
|
|
||||||
|
# Use the best title similarity (direct or cleaned)
|
||||||
|
best_title_similarity = max(title_similarity, clean_title_similarity)
|
||||||
|
|
||||||
|
# Weight: 50% title, 50% artist (equal weight to prevent false positives)
|
||||||
|
# Also require minimum artist similarity to prevent matching wrong artists
|
||||||
|
confidence = (best_title_similarity * 0.5) + (artist_similarity * 0.5)
|
||||||
|
|
||||||
|
# Apply artist similarity penalty: if artist match is too low, drastically reduce confidence
|
||||||
|
if artist_similarity < 0.6: # Less than 60% artist match
|
||||||
|
confidence *= 0.3 # Reduce confidence by 70%
|
||||||
|
|
||||||
|
return confidence
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error calculating track confidence: {e}")
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def _clean_track_title_for_comparison(self, title: str) -> str:
|
||||||
|
"""Clean track title for comparison by removing common noise"""
|
||||||
|
cleaned = title.lower().strip()
|
||||||
|
|
||||||
|
# Remove common patterns that cause mismatches
|
||||||
|
patterns_to_remove = [
|
||||||
|
r'\s*\(.*\)', # Remove anything in parentheses
|
||||||
|
r'\s*\[.*\]', # Remove anything in brackets
|
||||||
|
r'\s*-\s*.*', # Remove everything after dash
|
||||||
|
r'\s*feat\..*', # Remove featuring artists
|
||||||
|
r'\s*ft\..*', # Remove ft. artists
|
||||||
|
r'\s*featuring.*', # Remove featuring
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in patterns_to_remove:
|
||||||
|
cleaned = re.sub(pattern, '', cleaned, flags=re.IGNORECASE).strip()
|
||||||
|
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
def _clean_album_title_for_comparison(self, title: str) -> str:
|
||||||
|
"""Clean album title by removing edition markers for comparison"""
|
||||||
|
cleaned = title.lower()
|
||||||
|
|
||||||
|
# Remove common edition patterns
|
||||||
|
patterns = [
|
||||||
|
r'\s*\(deluxe\s*edition?\)',
|
||||||
|
r'\s*\(expanded\s*edition?\)',
|
||||||
|
r'\s*\(platinum\s*edition?\)',
|
||||||
|
r'\s*\(special\s*edition?\)',
|
||||||
|
r'\s*\(remastered?\)',
|
||||||
|
r'\s*\(anniversary\s*edition?\)',
|
||||||
|
r'\s*\(.*version\)',
|
||||||
|
r'\s*-\s*deluxe\s*edition?',
|
||||||
|
r'\s*-\s*platinum\s*edition?',
|
||||||
|
r'\s+deluxe\s*edition?$',
|
||||||
|
r'\s+platinum\s*edition?$',
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
cleaned = re.sub(pattern, '', cleaned, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
return cleaned.strip()
|
||||||
|
|
||||||
def get_album_completion_stats(self, artist_name: str) -> Dict[str, int]:
|
def get_album_completion_stats(self, artist_name: str) -> Dict[str, int]:
|
||||||
"""
|
"""
|
||||||
Get completion statistics for all albums by an artist.
|
Get completion statistics for all albums by an artist.
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1128,30 +1128,17 @@ class AlbumCard(QFrame):
|
||||||
self.overlay.setFixedSize(164, 164)
|
self.overlay.setFixedSize(164, 164)
|
||||||
self.overlay.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self.overlay.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
if self.is_owned:
|
# Set up initial overlay appearance (will be updated by update_ownership)
|
||||||
self.overlay.setStyleSheet("""
|
self.overlay.setStyleSheet("""
|
||||||
QLabel {
|
QLabel {
|
||||||
background: rgba(29, 185, 84, 0.8);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 24px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
self.overlay.setText("✓")
|
self.overlay.setText("Loading...")
|
||||||
else:
|
|
||||||
self.overlay.setStyleSheet("""
|
|
||||||
QLabel {
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: white;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
self.overlay.setText("📥\nDownload")
|
|
||||||
self.overlay.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
||||||
|
|
||||||
self.overlay.hide() # Initially hidden, shown on hover
|
self.overlay.hide() # Initially hidden, shown on hover
|
||||||
|
|
||||||
# Download progress overlay (shown during downloads)
|
# Download progress overlay (shown during downloads)
|
||||||
|
|
@ -1195,6 +1182,9 @@ class AlbumCard(QFrame):
|
||||||
layout.addWidget(album_label)
|
layout.addWidget(album_label)
|
||||||
layout.addWidget(year_label)
|
layout.addWidget(year_label)
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
|
|
||||||
|
# Initialize overlay text based on current ownership status
|
||||||
|
self._refresh_overlay_text()
|
||||||
|
|
||||||
def load_album_image(self):
|
def load_album_image(self):
|
||||||
"""Load album image in background"""
|
"""Load album image in background"""
|
||||||
|
|
@ -1216,14 +1206,98 @@ class AlbumCard(QFrame):
|
||||||
|
|
||||||
def enterEvent(self, event):
|
def enterEvent(self, event):
|
||||||
"""Show overlay on hover"""
|
"""Show overlay on hover"""
|
||||||
self.overlay.show()
|
try:
|
||||||
|
if hasattr(self, 'overlay') and self.overlay:
|
||||||
|
self.overlay.show()
|
||||||
|
self.overlay.raise_() # Bring to front
|
||||||
|
except (RuntimeError, AttributeError):
|
||||||
|
# Object has been deleted or is invalid, skip
|
||||||
|
pass
|
||||||
super().enterEvent(event)
|
super().enterEvent(event)
|
||||||
|
|
||||||
def leaveEvent(self, event):
|
def leaveEvent(self, event):
|
||||||
"""Hide overlay when not hovering"""
|
"""Hide overlay when not hovering"""
|
||||||
self.overlay.hide()
|
try:
|
||||||
|
if hasattr(self, 'overlay') and self.overlay:
|
||||||
|
self.overlay.hide()
|
||||||
|
except (RuntimeError, AttributeError):
|
||||||
|
# Object has been deleted or is invalid, skip
|
||||||
|
pass
|
||||||
super().leaveEvent(event)
|
super().leaveEvent(event)
|
||||||
|
|
||||||
|
def _refresh_overlay_text(self):
|
||||||
|
"""Refresh overlay text based on current ownership status"""
|
||||||
|
if self.is_owned:
|
||||||
|
if self.ownership_status and self.ownership_status.is_complete:
|
||||||
|
# Complete album (90%+) - green checkmark overlay
|
||||||
|
self.overlay.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
background: rgba(29, 185, 84, 0.8);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.overlay.setText("✓ Complete\nVerify tracks")
|
||||||
|
self.overlay.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
elif self.ownership_status and self.ownership_status.is_nearly_complete:
|
||||||
|
# Nearly complete album (80-89%) - blue overlay
|
||||||
|
self.overlay.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
background: rgba(13, 110, 253, 0.8);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
percentage = int(self.ownership_status.completion_ratio * 100)
|
||||||
|
missing_tracks = self.ownership_status.expected_tracks - self.ownership_status.owned_tracks
|
||||||
|
self.overlay.setText(f"◐ Nearly Complete\n({percentage}%)\nGet {missing_tracks} missing")
|
||||||
|
self.overlay.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
elif self.ownership_status:
|
||||||
|
# Partial album (<80%) - yellow warning overlay
|
||||||
|
self.overlay.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
background: rgba(255, 193, 7, 0.8);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #212529;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
percentage = int(self.ownership_status.completion_ratio * 100)
|
||||||
|
missing_tracks = self.ownership_status.expected_tracks - self.ownership_status.owned_tracks
|
||||||
|
self.overlay.setText(f"⚠ Partial\n({percentage}%)\nGet {missing_tracks} missing")
|
||||||
|
self.overlay.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
else:
|
||||||
|
# Legacy complete album - green checkmark overlay
|
||||||
|
self.overlay.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
background: rgba(29, 185, 84, 0.8);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.overlay.setText("✓ Complete\nVerify tracks")
|
||||||
|
self.overlay.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
else:
|
||||||
|
# Missing album - download overlay
|
||||||
|
self.overlay.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.overlay.setText("📥 Missing\n(0%)\nDownload")
|
||||||
|
self.overlay.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
|
||||||
def update_status_indicator(self):
|
def update_status_indicator(self):
|
||||||
"""Update the permanent status indicator"""
|
"""Update the permanent status indicator"""
|
||||||
if self.is_owned:
|
if self.is_owned:
|
||||||
|
|
@ -1319,83 +1393,25 @@ class AlbumCard(QFrame):
|
||||||
self.update_status_indicator()
|
self.update_status_indicator()
|
||||||
|
|
||||||
# Update the hover overlay
|
# Update the hover overlay
|
||||||
if self.is_owned:
|
self._refresh_overlay_text()
|
||||||
if self.ownership_status and self.ownership_status.is_complete:
|
|
||||||
# Complete album (90%+) - green checkmark overlay
|
|
||||||
self.overlay.setStyleSheet("""
|
|
||||||
QLabel {
|
|
||||||
background: rgba(29, 185, 84, 0.8);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: white;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
self.overlay.setText("✓ Complete\nVerify tracks")
|
|
||||||
self.overlay.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
||||||
elif self.ownership_status and self.ownership_status.is_nearly_complete:
|
|
||||||
# Nearly complete album (80-89%) - blue overlay
|
|
||||||
self.overlay.setStyleSheet("""
|
|
||||||
QLabel {
|
|
||||||
background: rgba(13, 110, 253, 0.8);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: white;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
percentage = int(self.ownership_status.completion_ratio * 100)
|
|
||||||
missing_tracks = self.ownership_status.expected_tracks - self.ownership_status.owned_tracks
|
|
||||||
self.overlay.setText(f"◐ {percentage}%\nGet {missing_tracks} missing")
|
|
||||||
self.overlay.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
||||||
elif self.ownership_status:
|
|
||||||
# Partial album (<80%) - yellow warning overlay
|
|
||||||
self.overlay.setStyleSheet("""
|
|
||||||
QLabel {
|
|
||||||
background: rgba(255, 193, 7, 0.8);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #212529;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
percentage = int(self.ownership_status.completion_ratio * 100)
|
|
||||||
missing_tracks = self.ownership_status.expected_tracks - self.ownership_status.owned_tracks
|
|
||||||
self.overlay.setText(f"⚠ {percentage}%\nGet {missing_tracks} missing")
|
|
||||||
self.overlay.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
||||||
else:
|
|
||||||
# Legacy complete album - green checkmark overlay
|
|
||||||
self.overlay.setStyleSheet("""
|
|
||||||
QLabel {
|
|
||||||
background: rgba(29, 185, 84, 0.8);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: white;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
self.overlay.setText("✓ Complete\nVerify tracks")
|
|
||||||
self.overlay.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
||||||
else:
|
|
||||||
# Missing album - download overlay
|
|
||||||
self.overlay.setStyleSheet("""
|
|
||||||
QLabel {
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: white;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
self.overlay.setText("📥\nDownload")
|
|
||||||
self.overlay.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
||||||
|
|
||||||
def set_download_in_progress(self):
|
def set_download_in_progress(self):
|
||||||
"""Set album card to download in progress state"""
|
"""Set album card to download in progress state"""
|
||||||
# Hide hover overlay and show progress overlay
|
# Hide hover overlay and show progress overlay
|
||||||
self.overlay.hide()
|
try:
|
||||||
self.progress_overlay.setText("⏳\nPreparing...")
|
if hasattr(self, 'overlay') and self.overlay:
|
||||||
self.progress_overlay.show()
|
self.overlay.hide()
|
||||||
|
except (RuntimeError, AttributeError):
|
||||||
|
# Object has been deleted or is invalid, skip
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(self, 'progress_overlay') and self.progress_overlay and not self.progress_overlay.isNull():
|
||||||
|
self.progress_overlay.setText("⏳\nPreparing...")
|
||||||
|
self.progress_overlay.show()
|
||||||
|
except (RuntimeError, AttributeError):
|
||||||
|
# Object has been deleted or is invalid, skip
|
||||||
|
pass
|
||||||
|
|
||||||
# Update status indicator
|
# Update status indicator
|
||||||
self.status_indicator.setStyleSheet("""
|
self.status_indicator.setStyleSheet("""
|
||||||
|
|
|
||||||
|
|
@ -223,10 +223,10 @@ class PlaylistTrackAnalysisWorker(QRunnable):
|
||||||
for query_title in unique_title_variations:
|
for query_title in unique_title_variations:
|
||||||
if self._cancelled: return None, 0.0
|
if self._cancelled: return None, 0.0
|
||||||
|
|
||||||
# Use database check_track_exists method
|
# Use database check_track_exists method with consistent thresholds
|
||||||
db_track, confidence = db.check_track_exists(query_title, artist_name, confidence_threshold=0.7)
|
db_track, confidence = db.check_track_exists(query_title, artist_name, confidence_threshold=0.7)
|
||||||
|
|
||||||
if db_track and confidence >= 0.8:
|
if db_track and confidence >= 0.7:
|
||||||
print(f"✔️ Database match found for '{original_title}' by '{artist_name}': '{db_track.title}' with confidence {confidence:.2f}")
|
print(f"✔️ Database match found for '{original_title}' by '{artist_name}': '{db_track.title}' with confidence {confidence:.2f}")
|
||||||
|
|
||||||
# Convert database track to format compatible with existing code
|
# Convert database track to format compatible with existing code
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue