diff --git a/core/__pycache__/plex_client.cpython-310.pyc b/core/__pycache__/plex_client.cpython-310.pyc index ec9bf167..4573ab7d 100644 Binary files a/core/__pycache__/plex_client.cpython-310.pyc and b/core/__pycache__/plex_client.cpython-310.pyc differ diff --git a/core/plex_client.py b/core/plex_client.py index 279138dc..e5079e31 100644 --- a/core/plex_client.py +++ b/core/plex_client.py @@ -472,6 +472,28 @@ class PlexClient: logger.error(f"Error updating poster for {artist.title}: {e}") return False + def update_album_poster(self, album, image_data: bytes): + """Update album poster image""" + try: + # Upload poster using Plex API + upload_url = f"{self.server._baseurl}/library/metadata/{album.ratingKey}/posters" + headers = { + 'X-Plex-Token': self.server._token, + 'Content-Type': 'image/jpeg' + } + + response = requests.post(upload_url, data=image_data, headers=headers) + response.raise_for_status() + + # Refresh album to see changes + album.refresh() + logger.info(f"Updated poster for album '{album.title}' by '{album.parentTitle}'") + return True + + except Exception as e: + logger.error(f"Error updating poster for album '{album.title}': {e}") + return False + def parse_update_timestamp(self, artist: PlexArtist) -> Optional[datetime]: """Parse the last update timestamp from artist summary""" try: diff --git a/ui/pages/__pycache__/dashboard.cpython-310.pyc b/ui/pages/__pycache__/dashboard.cpython-310.pyc new file mode 100644 index 00000000..ac5971b6 Binary files /dev/null and b/ui/pages/__pycache__/dashboard.cpython-310.pyc differ diff --git a/ui/pages/__pycache__/dashboard.cpython-312.pyc b/ui/pages/__pycache__/dashboard.cpython-312.pyc index 0a872023..fc0a2e30 100644 Binary files a/ui/pages/__pycache__/dashboard.cpython-312.pyc and b/ui/pages/__pycache__/dashboard.cpython-312.pyc differ diff --git a/ui/pages/dashboard.py b/ui/pages/dashboard.py index 1ade1960..6294896b 100644 --- a/ui/pages/dashboard.py +++ b/ui/pages/dashboard.py @@ -175,6 +175,11 @@ class MetadataUpdateWorker(QThread): if genres_updated: changes_made.append("genres") + # Update album artwork + albums_updated = self.update_album_artwork(artist, spotify_artist) + if albums_updated > 0: + changes_made.append(f"{albums_updated} album art") + if changes_made: # Update artist biography with timestamp to track last update biography_updated = self.plex_client.update_artist_biography(artist) @@ -271,6 +276,162 @@ class MetadataUpdateWorker(QThread): print(f"Error updating genres for {getattr(artist, 'title', 'Unknown')}: {e}") return False + def update_album_artwork(self, artist, spotify_artist): + """Update album artwork for all albums by this artist""" + try: + updated_count = 0 + skipped_count = 0 + + # Get all albums for this artist + try: + albums = list(artist.albums()) + except Exception: + print(f"Could not access albums for artist '{artist.title}'") + return 0 + + if not albums: + print(f"No albums found for artist '{artist.title}'") + return 0 + + print(f"🎨 Checking artwork for {len(albums)} albums by '{artist.title}'...") + + for album in albums: + try: + album_title = getattr(album, 'title', 'Unknown Album') + + # Check if album already has good artwork (debug=True to see detection logic) + if self.album_has_valid_artwork(album, debug=True): + skipped_count += 1 + continue + + print(f"Album '{album_title}' needs artwork - searching Spotify...") + + # Search for this specific album on Spotify + album_query = f"album:{album_title} artist:{spotify_artist.name}" + spotify_albums = self.spotify_client.search_albums(album_query, limit=3) + + if not spotify_albums: + print(f"No Spotify results for album '{album_title}'") + continue + + # Find the best matching album + best_album = None + highest_score = 0.0 + + plex_album_normalized = self.matching_engine.normalize_string(album_title) + + for spotify_album in spotify_albums: + spotify_album_normalized = self.matching_engine.normalize_string(spotify_album.name) + score = self.matching_engine.similarity_score(plex_album_normalized, spotify_album_normalized) + + if score > highest_score: + highest_score = score + best_album = spotify_album + + # If we found a good match with artwork, download it + if best_album and highest_score > 0.7 and best_album.image_url: + print(f"Found Spotify match: '{best_album.name}' (score: {highest_score:.2f})") + + # Download and upload the artwork + if self.download_and_upload_album_artwork(album, best_album.image_url): + updated_count += 1 + + else: + print(f"No good Spotify match for album '{album_title}' (best score: {highest_score:.2f})") + + except Exception as e: + print(f"Error processing album '{getattr(album, 'title', 'Unknown')}': {e}") + continue + + total_processed = updated_count + skipped_count + print(f"🎨 Artwork summary for '{artist.title}': {updated_count} updated, {skipped_count} skipped (already have good artwork)") + + if updated_count == 0 and skipped_count == len(albums): + print(f" ✅ All albums already have good artwork - no Spotify API calls needed!") + return updated_count + + except Exception as e: + print(f"Error updating album artwork for artist '{getattr(artist, 'title', 'Unknown')}': {e}") + return 0 + + def album_has_valid_artwork(self, album, debug=False): + """Check if album has valid artwork - conservative approach""" + try: + album_title = getattr(album, 'title', 'Unknown Album') + + # Check if album has any thumb at all + if not hasattr(album, 'thumb') or not album.thumb: + if debug: print(f" 🎨 Album '{album_title}' has NO THUMB - needs update") + return False + + thumb_url = str(album.thumb) + if debug: print(f" 🔍 Album '{album_title}' artwork URL: {thumb_url}") + + # CONSERVATIVE APPROACH: Only mark as "needs update" in very obvious cases + + # Case 1: Completely empty or None + if not thumb_url or thumb_url.strip() == '': + if debug: print(f" 🎨 Album '{album_title}' has empty URL - needs update") + return False + + # Case 2: Obvious placeholder text in URL + obvious_placeholders = [ + 'no-image', + 'placeholder', + 'missing', + 'default-album', + 'blank.jpg', + 'empty.png' + ] + + thumb_lower = thumb_url.lower() + for placeholder in obvious_placeholders: + if placeholder in thumb_lower: + if debug: print(f" 🎨 Album '{album_title}' has obvious placeholder ({placeholder}) - needs update") + return False + + # Case 3: Extremely short URLs (likely broken) + if len(thumb_url) < 20: + if debug: print(f" 🎨 Album '{album_title}' has very short URL ({len(thumb_url)} chars) - needs update") + return False + + # OTHERWISE: Assume it has valid artwork and SKIP updating + if debug: print(f" ✅ Album '{album_title}' appears to have artwork - SKIPPING (URL: {len(thumb_url)} chars)") + return True + + except Exception as e: + if debug: print(f" ❌ Error checking artwork for album '{album_title}': {e}") + # If we can't check, be conservative and skip updating + return True + + def download_and_upload_album_artwork(self, album, image_url): + """Download artwork from Spotify and upload to Plex""" + try: + album_title = getattr(album, 'title', 'Unknown Album') + + # Download image from Spotify + response = requests.get(image_url, timeout=10) + response.raise_for_status() + + # Validate and convert image (reuse existing function) + image_data = self.validate_and_convert_image(response.content) + if not image_data: + print(f"Invalid image data for album '{album_title}'") + return False + + # Upload to Plex using our new method + success = self.plex_client.update_album_poster(album, image_data) + if success: + print(f"✅ Updated artwork for album '{album_title}'") + else: + print(f"❌ Failed to upload artwork for album '{album_title}'") + + return success + + except Exception as e: + print(f"Error downloading/uploading artwork for album '{getattr(album, 'title', 'Unknown')}': {e}") + return False + def artist_has_valid_photo(self, artist): """Check if artist has a valid photo""" try: