include deep search for artists.

pull/2/head
Broque Thomas 9 months ago
parent 7212e9224c
commit 78d037cba4

@ -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:

@ -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):

Loading…
Cancel
Save