diff --git a/database/__pycache__/music_database.cpython-312.pyc b/database/__pycache__/music_database.cpython-312.pyc index b392b24c..a015a527 100644 Binary files a/database/__pycache__/music_database.cpython-312.pyc and b/database/__pycache__/music_database.cpython-312.pyc differ diff --git a/database/music_database.py b/database/music_database.py index a050c81b..9fd2a263 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -693,6 +693,115 @@ class MusicDatabase: return max(0.0, similarity) + def check_album_completeness(self, album_id: int, expected_track_count: Optional[int] = None) -> Tuple[int, int, bool]: + """ + Check if we have all tracks for an album. + Returns (owned_tracks, expected_tracks, is_complete) + """ + try: + conn = self._get_connection() + cursor = conn.cursor() + + # Get actual track count in our database + cursor.execute("SELECT COUNT(*) FROM tracks WHERE album_id = ?", (album_id,)) + owned_tracks = cursor.fetchone()[0] + + # Get expected track count from album table + cursor.execute("SELECT track_count FROM albums WHERE id = ?", (album_id,)) + result = cursor.fetchone() + + if not result: + return 0, 0, False + + stored_track_count = result[0] + + # Use provided expected count if available, otherwise use stored count + expected_tracks = expected_track_count if expected_track_count is not None else stored_track_count + + # 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 + else: + # Fallback: if we have any tracks, consider it owned + is_complete = owned_tracks > 0 + + return owned_tracks, expected_tracks or 0, is_complete + + except Exception as e: + logger.error(f"Error checking album completeness for album_id {album_id}: {e}") + return 0, 0, False + + 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. + Returns (album, confidence, owned_tracks, expected_tracks, is_complete) + """ + try: + # First find the album match + album, confidence = self.check_album_exists(title, artist, confidence_threshold) + + if not album: + return None, 0.0, 0, 0, False + + # Now check completeness + owned_tracks, expected_tracks, is_complete = self.check_album_completeness(album.id, expected_track_count) + + return album, confidence, owned_tracks, expected_tracks, is_complete + + except Exception as e: + logger.error(f"Error checking album existence with completeness for '{title}' by '{artist}': {e}") + return None, 0.0, 0, 0, False + + def get_album_completion_stats(self, artist_name: str) -> Dict[str, int]: + """ + Get completion statistics for all albums by an artist. + Returns dict with counts of complete, partial, and missing albums. + """ + try: + conn = self._get_connection() + cursor = conn.cursor() + + # Get all albums by this artist with track counts + cursor.execute(""" + SELECT albums.id, albums.track_count, COUNT(tracks.id) as actual_tracks + FROM albums + JOIN artists ON albums.artist_id = artists.id + LEFT JOIN tracks ON albums.id = tracks.album_id + WHERE artists.name LIKE ? + GROUP BY albums.id, albums.track_count + """, (f"%{artist_name}%",)) + + results = cursor.fetchall() + stats = { + 'complete': 0, # >=90% of tracks + 'nearly_complete': 0, # 80-89% of tracks + 'partial': 0, # 1-79% of tracks + 'missing': 0, # 0% of tracks + 'total': len(results) + } + + for row in results: + expected_tracks = row['track_count'] or 1 # Avoid division by zero + actual_tracks = row['actual_tracks'] + completion_ratio = actual_tracks / expected_tracks + + if actual_tracks == 0: + stats['missing'] += 1 + elif completion_ratio >= 0.9: + stats['complete'] += 1 + elif completion_ratio >= 0.8: + stats['nearly_complete'] += 1 + else: + stats['partial'] += 1 + + return stats + + except Exception as e: + logger.error(f"Error getting album completion stats for artist '{artist_name}': {e}") + return {'complete': 0, 'nearly_complete': 0, 'partial': 0, 'missing': 0, 'total': 0} + def get_database_info(self) -> Dict[str, Any]: """Get comprehensive database information""" try: diff --git a/ui/pages/__pycache__/artists.cpython-312.pyc b/ui/pages/__pycache__/artists.cpython-312.pyc index 4adfb4ab..62a7ca02 100644 Binary files a/ui/pages/__pycache__/artists.cpython-312.pyc and b/ui/pages/__pycache__/artists.cpython-312.pyc differ diff --git a/ui/pages/artists.py b/ui/pages/artists.py index 9446097f..d968c229 100644 --- a/ui/pages/artists.py +++ b/ui/pages/artists.py @@ -29,6 +29,29 @@ class ArtistMatch: confidence: float match_reason: str = "" +@dataclass +class AlbumOwnershipStatus: + """Represents album ownership status with completeness info""" + album_name: str + is_owned: bool + is_complete: bool + is_nearly_complete: bool + owned_tracks: int + expected_tracks: int + completion_ratio: float + + @property + def completion_level(self) -> str: + """Get completion level as string""" + if not self.is_owned: + return "missing" + elif self.completion_ratio >= 0.9: + return "complete" + elif self.completion_ratio >= 0.8: + return "nearly_complete" + else: + return "partial" + class DownloadCompletionWorkerSignals(QObject): """Signals for the download completion worker""" completed = pyqtSignal(object, str) # download_item, organized_path @@ -423,9 +446,9 @@ class AlbumStatusProcessingWorker(QRunnable): class DatabaseLibraryWorker(QThread): - """Background worker for checking database library (replaces PlexLibraryWorker)""" - library_checked = pyqtSignal(set) # Set of owned album names (final result) - album_matched = pyqtSignal(str) # Individual album match (album name) + """Background worker for checking database library with completeness info (replaces PlexLibraryWorker)""" + library_checked = pyqtSignal(dict) # Dict of album_name -> AlbumOwnershipStatus + album_matched = pyqtSignal(str, object) # album_name, AlbumOwnershipStatus check_failed = pyqtSignal(str) def __init__(self, albums, matching_engine): @@ -440,8 +463,8 @@ class DatabaseLibraryWorker(QThread): def run(self): try: - print("🔍 Starting robust database album matching...") - owned_albums = set() + print("🔍 Starting robust database album matching with completeness checking...") + album_statuses = {} # album_name -> AlbumOwnershipStatus # Get database instance db = get_database() @@ -472,8 +495,14 @@ class DatabaseLibraryWorker(QThread): # Try different artist combinations artists_to_try = spotify_album.artists[:2] if spotify_album.artists else [""] - best_match = None + best_album = None best_confidence = 0.0 + best_owned_tracks = 0 + best_expected_tracks = 0 + best_is_complete = False + + # Get expected track count from Spotify + expected_track_count = getattr(spotify_album, 'total_tracks', None) # Search with different combinations for artist in artists_to_try: @@ -486,61 +515,112 @@ class DatabaseLibraryWorker(QThread): if self._stop_requested: return - # Search database for this combination (cleaned artist) + # Search database for this combination with completeness info print(f" 🔍 Searching database: album='{album_name}', artist='{artist_clean}'") - db_album, confidence = db.check_album_exists(album_name, artist_clean, confidence_threshold=0.7) + db_album, confidence, owned_tracks, expected_tracks, is_complete = db.check_album_exists_with_completeness( + album_name, artist_clean, expected_track_count, confidence_threshold=0.7 + ) if db_album and confidence > best_confidence: - best_match = db_album + best_album = db_album best_confidence = confidence - print(f" 📀 Found database match with confidence {confidence:.2f}") + best_owned_tracks = owned_tracks + best_expected_tracks = expected_tracks + best_is_complete = is_complete + print(f" 📀 Found database match with confidence {confidence:.2f} ({owned_tracks}/{expected_tracks} tracks)") # If we have a very confident match, we can stop searching for this album if confidence >= 0.95: break - # Backup search with original uncleaned artist name (for cases like "Tyler, The Creator") + # Backup search with original uncleaned artist name if not db_album and artist and artist != artist_clean: print(f" 🔄 Backup search with original artist: album='{album_name}', artist='{artist}'") - db_album_backup, confidence_backup = db.check_album_exists(album_name, artist, confidence_threshold=0.7) + db_album_backup, confidence_backup, owned_backup, expected_backup, complete_backup = db.check_album_exists_with_completeness( + album_name, artist, expected_track_count, confidence_threshold=0.7 + ) if db_album_backup and confidence_backup > best_confidence: - best_match = db_album_backup + best_album = db_album_backup best_confidence = confidence_backup - print(f" 📀 Found backup match with confidence {confidence_backup:.2f}") + best_owned_tracks = owned_backup + best_expected_tracks = expected_backup + best_is_complete = complete_backup + print(f" 📀 Found backup match with confidence {confidence_backup:.2f} ({owned_backup}/{expected_backup} tracks)") - # Additional fallback: remove commas (Tyler, The Creator -> Tyler The Creator) + # Additional fallback: remove commas if not db_album_backup and ',' in artist: artist_no_comma = artist.replace(',', '').strip() - # Clean up multiple spaces that might result from comma removal artist_no_comma = ' '.join(artist_no_comma.split()) print(f" 🔄 Comma-removal fallback: album='{album_name}', artist='{artist_no_comma}'") - db_album_comma, confidence_comma = db.check_album_exists(album_name, artist_no_comma, confidence_threshold=0.7) + db_album_comma, confidence_comma, owned_comma, expected_comma, complete_comma = db.check_album_exists_with_completeness( + album_name, artist_no_comma, expected_track_count, confidence_threshold=0.7 + ) if db_album_comma and confidence_comma > best_confidence: - best_match = db_album_comma + best_album = db_album_comma best_confidence = confidence_comma - print(f" 📀 Found comma-removal match with confidence {confidence_comma:.2f}") + best_owned_tracks = owned_comma + best_expected_tracks = expected_comma + best_is_complete = complete_comma + print(f" 📀 Found comma-removal match with confidence {confidence_comma:.2f} ({owned_comma}/{expected_comma} tracks)") # If we found a very confident match, stop searching other artists if best_confidence >= 0.95: break - # Check final result - if best_match and best_confidence >= 0.8: - owned_albums.add(spotify_album.name) - print(f"✅ Database match found: '{spotify_album.name}' -> '{best_match.title}' (confidence: {best_confidence:.2f})") + # Create ownership status + if best_album and best_confidence >= 0.8: + completion_ratio = best_owned_tracks / max(best_expected_tracks, 1) + is_nearly_complete = completion_ratio >= 0.8 and completion_ratio < 0.9 + status = AlbumOwnershipStatus( + album_name=spotify_album.name, + is_owned=True, + is_complete=best_is_complete, + is_nearly_complete=is_nearly_complete, + owned_tracks=best_owned_tracks, + expected_tracks=best_expected_tracks, + completion_ratio=completion_ratio + ) + album_statuses[spotify_album.name] = status + + # Log detailed result + if best_is_complete: + print(f"✅ Complete album: '{spotify_album.name}' -> '{best_album.title}' ({best_owned_tracks}/{best_expected_tracks} tracks)") + elif is_nearly_complete: + print(f"🔵 Nearly complete album: '{spotify_album.name}' -> '{best_album.title}' ({best_owned_tracks}/{best_expected_tracks} tracks)") + else: + print(f"⚠️ Partial album: '{spotify_album.name}' -> '{best_album.title}' ({best_owned_tracks}/{best_expected_tracks} tracks)") + # Emit individual match for real-time UI update - self.album_matched.emit(spotify_album.name) + self.album_matched.emit(spotify_album.name, status) else: - if best_match: + # Create status for missing album + status = AlbumOwnershipStatus( + album_name=spotify_album.name, + is_owned=False, + is_complete=False, + is_nearly_complete=False, + owned_tracks=0, + expected_tracks=expected_track_count or 0, + completion_ratio=0.0 + ) + album_statuses[spotify_album.name] = status + + if best_album: print(f"❌ No confident match for '{spotify_album.name}' (best: {best_confidence:.2f})") else: print(f"❌ No database candidates found for '{spotify_album.name}'") - print(f"🎯 Final result: {len(owned_albums)} owned albums out of {len(self.albums)}") - print(f"🚀 Emitting signal with owned_albums: {list(owned_albums)}") - self.library_checked.emit(owned_albums) + # Count results for summary + complete_count = sum(1 for status in album_statuses.values() if status.is_complete) + nearly_complete_count = sum(1 for status in album_statuses.values() if status.is_nearly_complete) + partial_count = sum(1 for status in album_statuses.values() if status.is_owned and not status.is_complete and not status.is_nearly_complete) + missing_count = sum(1 for status in album_statuses.values() if not status.is_owned) + + print(f"🎯 Final result: {complete_count} complete, {nearly_complete_count} nearly complete, {partial_count} partial, {missing_count} missing out of {len(self.albums)} albums") + print(f"🚀 Emitting detailed album statuses") + self.library_checked.emit(album_statuses) except Exception as e: if not self._stop_requested: @@ -985,6 +1065,7 @@ class AlbumCard(QFrame): super().__init__(parent) self.album = album self.is_owned = is_owned + self.ownership_status = None # Will store AlbumOwnershipStatus self.setup_ui() self.load_album_image() @@ -1146,18 +1227,63 @@ class AlbumCard(QFrame): def update_status_indicator(self): """Update the permanent status indicator""" if self.is_owned: - self.status_indicator.setStyleSheet(""" - QLabel { - background: rgba(29, 185, 84, 0.9); - border-radius: 12px; - color: white; - font-size: 14px; - font-weight: bold; - } - """) - self.status_indicator.setText("✓") - self.status_indicator.setToolTip("Album owned in Plex") + if self.ownership_status and self.ownership_status.is_complete: + # Complete album (90%+) - green checkmark + self.status_indicator.setStyleSheet(""" + QLabel { + background: rgba(29, 185, 84, 0.9); + border-radius: 12px; + color: white; + font-size: 14px; + font-weight: bold; + } + """) + self.status_indicator.setText("✓") + self.status_indicator.setToolTip(f"Complete album - {self.ownership_status.owned_tracks}/{self.ownership_status.expected_tracks} tracks ({int(self.ownership_status.completion_ratio * 100)}%)") + elif self.ownership_status and self.ownership_status.is_nearly_complete: + # Nearly complete album (80-89%) - blue half-circle + self.status_indicator.setStyleSheet(""" + QLabel { + background: rgba(13, 110, 253, 0.9); + border-radius: 12px; + color: white; + font-size: 14px; + font-weight: bold; + } + """) + self.status_indicator.setText("◐") + percentage = int(self.ownership_status.completion_ratio * 100) + missing_tracks = self.ownership_status.expected_tracks - self.ownership_status.owned_tracks + self.status_indicator.setToolTip(f"Nearly complete - {self.ownership_status.owned_tracks}/{self.ownership_status.expected_tracks} tracks ({percentage}%) • {missing_tracks} missing") + elif self.ownership_status and not self.ownership_status.is_complete and not self.ownership_status.is_nearly_complete: + # Partial album (<80%) - yellow warning + self.status_indicator.setStyleSheet(""" + QLabel { + background: rgba(255, 193, 7, 0.9); + border-radius: 12px; + color: #212529; + font-size: 14px; + font-weight: bold; + } + """) + self.status_indicator.setText("⚠") + percentage = int(self.ownership_status.completion_ratio * 100) + self.status_indicator.setToolTip(f"Partial album - {self.ownership_status.owned_tracks}/{self.ownership_status.expected_tracks} tracks ({percentage}%)") + else: + # Fallback for legacy owned albums without detailed status + self.status_indicator.setStyleSheet(""" + QLabel { + background: rgba(29, 185, 84, 0.9); + border-radius: 12px; + color: white; + font-size: 14px; + font-weight: bold; + } + """) + self.status_indicator.setText("✓") + self.status_indicator.setToolTip("Album owned in library") else: + # Missing album - red download icon self.status_indicator.setStyleSheet(""" QLabel { background: rgba(220, 53, 69, 0.8); @@ -1170,10 +1296,22 @@ class AlbumCard(QFrame): self.status_indicator.setText("📥") self.status_indicator.setToolTip("Album available for download") - def update_ownership(self, is_owned: bool): - """Update ownership status and refresh UI""" + def update_ownership(self, ownership_info): + """Update ownership status and refresh UI - supports bool or AlbumOwnershipStatus""" + if isinstance(ownership_info, bool): + # Legacy support for simple boolean + is_owned = ownership_info + self.ownership_status = None + else: + # New detailed ownership status + is_owned = ownership_info.is_owned + self.ownership_status = ownership_info + if self.is_owned != is_owned: # Only log if status actually changed - print(f"🔄 '{self.album.name}' ownership: {self.is_owned} -> {is_owned}") + if self.ownership_status: + print(f"🔄 '{self.album.name}' ownership: {self.is_owned} -> {is_owned} (complete: {self.ownership_status.is_complete})") + else: + print(f"🔄 '{self.album.name}' ownership: {self.is_owned} -> {is_owned}") self.is_owned = is_owned @@ -1182,18 +1320,64 @@ class AlbumCard(QFrame): # Update the hover overlay if self.is_owned: - self.overlay.setStyleSheet(""" - QLabel { - background: rgba(29, 185, 84, 0.8); - border-radius: 6px; - color: white; - font-size: 24px; - font-weight: bold; - } - """) - self.overlay.setText("✓") - self.overlay.setCursor(Qt.CursorShape.ArrowCursor) + 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); @@ -1294,6 +1478,7 @@ class AlbumCard(QFrame): # Don't allow downloads if already downloading if (event.button() == Qt.MouseButton.LeftButton and not self.progress_overlay.isVisible()): + print(f"🖱️ Album card clicked: {self.album.name} (owned: {self.is_owned})") self.download_requested.emit(self.album) super().mousePressEvent(event) @@ -2909,9 +3094,14 @@ class ArtistsPage(QWidget): # Start Plex library check in background - will update UI when complete self.start_plex_library_check(albums) - def display_albums(self, albums, owned_albums): - """Display albums in the grid""" - print(f"🎨 Displaying {len(albums)} albums, {len(owned_albums)} owned") + def display_albums(self, albums, ownership_info): + """Display albums in the grid - supports legacy set or new dict of AlbumOwnershipStatus""" + + # Handle both old format (set of owned album names) and new format (dict of statuses) + if isinstance(ownership_info, dict): + print(f"🎨 Displaying {len(albums)} albums with detailed ownership info") + else: + print(f"🎨 Displaying {len(albums)} albums, {len(ownership_info)} owned") # Clear existing albums self.clear_albums() @@ -2920,11 +3110,23 @@ class ArtistsPage(QWidget): max_cols = 5 for album in albums: - is_owned = album.name in owned_albums + if isinstance(ownership_info, dict): + # New format - use detailed ownership status + status = ownership_info.get(album.name) + if status: + card = AlbumCard(album, status.is_owned) + card.update_ownership(status) + else: + # Album not found in statuses - assume not owned + card = AlbumCard(album, False) + else: + # Legacy format - simple set of owned album names + is_owned = album.name in ownership_info + card = AlbumCard(album, is_owned) - card = AlbumCard(album, is_owned) - if not is_owned: - card.download_requested.connect(self.on_album_download_requested) + # Connect download signal for all albums - we can download missing tracks for partial albums + # and missing albums, but complete albums will show a different modal + card.download_requested.connect(self.on_album_download_requested) self.albums_grid_layout.addWidget(card, row, col) @@ -2952,33 +3154,60 @@ class ArtistsPage(QWidget): self.plex_library_worker.check_failed.connect(self.on_plex_library_check_failed) self.plex_library_worker.start() - def on_plex_library_checked(self, owned_albums): - """Handle final Plex library check completion""" - print(f"📨 Plex check completed: {len(owned_albums)} total matches") + def on_plex_library_checked(self, album_statuses): + """Handle final database library check completion with detailed status info""" + print(f"📨 Database check completed: {len(album_statuses)} album statuses") if not self.current_albums: print("📨 No current albums, skipping final update") return - # Update final status message - owned_count = len(owned_albums) + # Count different types of ownership + complete_count = sum(1 for status in album_statuses.values() if status.is_complete) + nearly_complete_count = sum(1 for status in album_statuses.values() if status.is_nearly_complete) + partial_count = sum(1 for status in album_statuses.values() if status.is_owned and not status.is_complete and not status.is_nearly_complete) + missing_count = sum(1 for status in album_statuses.values() if not status.is_owned) total_count = len(self.current_albums) - missing_count = total_count - owned_count - self.albums_status.setText(f"Found {total_count} albums • {owned_count} owned • {missing_count} available for download") + # Update final status message with all categories + status_parts = [] + if complete_count > 0: + status_parts.append(f"{complete_count} complete") + if nearly_complete_count > 0: + status_parts.append(f"{nearly_complete_count} nearly complete") + if partial_count > 0: + status_parts.append(f"{partial_count} partial") + if missing_count > 0: + status_parts.append(f"{missing_count} missing") - # Show toast with Plex check results + self.albums_status.setText(f"Found {total_count} albums • " + " • ".join(status_parts)) + + # Show toast with library check results if hasattr(self, 'toast_manager') and self.toast_manager: + owned_count = complete_count + nearly_complete_count + partial_count if owned_count == 0: - self.toast_manager.info(f"No albums found in your Plex library ({total_count} available for download)") + self.toast_manager.info(f"No albums found in your library ({total_count} available for download)") + elif nearly_complete_count > 0 or partial_count > 0: + if nearly_complete_count > 0: + self.toast_manager.success(f"Found {complete_count} complete, {nearly_complete_count} nearly complete, {partial_count} partial albums out of {total_count}") + else: + self.toast_manager.success(f"Found {complete_count} complete, {partial_count} partial albums out of {total_count}") else: - self.toast_manager.success(f"Found {owned_count} of {total_count} albums in your Plex library") + self.toast_manager.success(f"Found {complete_count} complete albums out of {total_count}") + + print(f"✅ Database check complete: {complete_count} complete, {nearly_complete_count} nearly complete, {partial_count} partial, {missing_count} missing out of {total_count} albums") - print(f"✅ Plex check complete: {owned_count}/{total_count} albums owned") + # Update the album display with the final ownership statuses + self.display_albums(self.current_albums, album_statuses) - def on_album_matched(self, album_name): - """Handle individual album match for real-time UI update""" - print(f"🎯 Real-time match: '{album_name}'") + def on_album_matched(self, album_name, ownership_status): + """Handle individual album match for real-time UI update with detailed status""" + if ownership_status.is_complete: + print(f"🎯 Real-time match: '{album_name}' (complete)") + elif ownership_status.is_nearly_complete: + print(f"🎯 Real-time match: '{album_name}' (nearly complete {int(ownership_status.completion_ratio * 100)}%)") + else: + print(f"🎯 Real-time match: '{album_name}' (partial {int(ownership_status.completion_ratio * 100)}%)") # Update match counter self.matched_count += 1 @@ -2995,8 +3224,14 @@ class ArtistsPage(QWidget): if item and item.widget(): album_card = item.widget() if hasattr(album_card, 'album') and album_card.album.name == album_name: - print(f"🔄 Real-time update: '{album_name}' -> owned") - album_card.update_ownership(True) + if ownership_status.is_complete: + status_text = "complete" + elif ownership_status.is_nearly_complete: + status_text = f"nearly complete ({int(ownership_status.completion_ratio * 100)}%)" + else: + status_text = f"partial ({int(ownership_status.completion_ratio * 100)}%)" + print(f"🔄 Real-time update: '{album_name}' -> {status_text}") + album_card.update_ownership(ownership_status) break def on_plex_library_check_failed(self, error):