diff --git a/core/jellyfin_client.py b/core/jellyfin_client.py index 1b2cef01..5e83be26 100644 --- a/core/jellyfin_client.py +++ b/core/jellyfin_client.py @@ -1186,15 +1186,9 @@ class JellyfinClient: # Metadata update methods for compatibility with metadata updater def update_artist_genres(self, artist, genres: List[str]): - """Update artist genres - currently not implemented for Jellyfin""" - try: - # TODO: Implement Jellyfin genre update API - # For now, just log and return success to continue processing - logger.debug(f"Genre update not yet implemented for Jellyfin artist: {artist.title}") - return True - except Exception as e: - logger.error(f"Error updating genres for {artist.title}: {e}") - return False + """Update artist genres - not implemented for Jellyfin""" + # Genre updates not supported via Jellyfin API - silently skip + return True def update_artist_poster(self, artist, image_data: bytes): """Update artist poster image using Jellyfin API""" @@ -1206,49 +1200,33 @@ class JellyfinClient: import requests url = f"{self.base_url}/Items/{artist_id}/Images/Primary" + + # Use the working approach from successful Jellyfin implementation + from base64 import b64encode + + # Base64 encode the image data (key difference!) + encoded_data = b64encode(image_data) + + # Add /0 to URL for image index + url = f"{self.base_url}/Items/{artist_id}/Images/Primary/0" + headers = { - 'X-Emby-Token': self.api_key + 'X-Emby-Token': self.api_key, + 'Content-Type': 'image/jpeg' } - - # Try multiple approaches to find what works with Jellyfin - - # Method 1: Try with different field names that Jellyfin might expect - method1_files = {'data': ('poster.jpg', image_data, 'image/jpeg')} - try: - response = requests.post(url, files=method1_files, headers=headers, timeout=30) - response.raise_for_status() - logger.info(f"Updated poster for {artist.title} (method 1)") - return True - except Exception as e1: - logger.debug(f"Method 1 failed for {artist.title}: {e1}") - - # Method 2: Try with raw data and proper content-type - try: - headers_raw = { - 'X-Emby-Token': self.api_key, - 'Content-Type': 'image/jpeg' - } - response = requests.post(url, data=image_data, headers=headers_raw, timeout=30) - response.raise_for_status() - logger.info(f"Updated poster for {artist.title} (method 2)") - return True - except Exception as e2: - logger.debug(f"Method 2 failed for {artist.title}: {e2}") - - # Method 3: Try with different endpoint structure + try: - alt_url = f"{self.base_url}/Items/{artist_id}/Images/Primary/0" - response = requests.post(alt_url, data=image_data, headers=headers_raw, timeout=30) + logger.debug(f"Uploading {len(image_data)} bytes (base64 encoded) for {artist.title}") + + response = requests.post(url, data=encoded_data, headers=headers, timeout=30) response.raise_for_status() - logger.info(f"Updated poster for {artist.title} (method 3)") + logger.info(f"Updated poster for {artist.title} - HTTP {response.status_code}") return True - except Exception as e3: - logger.debug(f"Method 3 failed for {artist.title}: {e3}") - - # All methods failed - logger.error(f"All image upload methods failed for {artist.title}") - return False - + + except Exception as e: + logger.error(f"Failed to upload poster for {artist.title}: {e}") + return False + except Exception as e: logger.error(f"Error updating poster for {artist.title}: {e}") return False @@ -1311,44 +1289,48 @@ class JellyfinClient: return False def update_artist_biography(self, artist) -> bool: - """Update artist overview/biography - currently not implemented for Jellyfin""" - try: - # TODO: Implement Jellyfin biography update API - # For now, just log and return success to continue processing - logger.debug(f"Biography update not yet implemented for Jellyfin artist: {artist.title}") - return True - except Exception as e: - logger.error(f"Error updating biography for {artist.title}: {e}") - return False + """Update artist overview/biography - not implemented for Jellyfin""" + # Biography updates not supported via Jellyfin API - silently skip + return True def needs_update_by_age(self, artist, refresh_interval_days: int) -> bool: - """Check if artist needs updating based on age threshold - simplified for Jellyfin""" + """Check if artist needs updating based on age threshold""" try: - # For now, just return True for all artists since we don't have timestamp tracking yet - # TODO: Implement timestamp tracking in Jellyfin artist metadata - return True + last_update = self.parse_update_timestamp(artist) + if not last_update: + # No timestamp found, needs update + return True + + # Calculate days since last update + from datetime import datetime + days_since_update = (datetime.now() - last_update).days + + # Use same logic as Plex client + needs_update = days_since_update >= refresh_interval_days + + if not needs_update: + logger.debug(f"Skipping {artist.title}: updated {days_since_update} days ago (threshold: {refresh_interval_days})") + + return needs_update + except Exception as e: logger.debug(f"Error checking update age for {artist.title}: {e}") - return True + return True # Default to needing update if error def is_artist_ignored(self, artist) -> bool: - """Check if artist is manually marked to be ignored - simplified for Jellyfin""" + """Check if artist is manually marked to be ignored""" try: - # For now, no artists are ignored - # TODO: Implement ignore flag tracking in Jellyfin artist metadata - return False + # Check overview field where we store timestamps and ignore flags + overview = getattr(artist, 'overview', '') or '' + return '-IgnoreUpdate' in overview except Exception as e: logger.debug(f"Error checking ignore status for {artist.title}: {e}") return False def parse_update_timestamp(self, artist) -> Optional[datetime]: - """Parse the last update timestamp from artist summary - not implemented for Jellyfin""" - try: - # TODO: Implement timestamp parsing from Jellyfin artist overview - return None - except Exception as e: - logger.debug(f"Error parsing timestamp for {artist.title}: {e}") - return None + """Parse the last update timestamp - not implemented for Jellyfin""" + # No timestamp tracking for Jellyfin - always return None (needs update) + return None def set_metadata_only_mode(self, enabled: bool = True): """Enable metadata-only mode to skip expensive track caching""" diff --git a/web_server.py b/web_server.py index 89bf55cb..efe96bf8 100644 --- a/web_server.py +++ b/web_server.py @@ -11635,12 +11635,17 @@ def start_metadata_update(): # Check active server and client availability - EXACTLY like dashboard.py active_server = config_manager.get_active_media_server() - # Get appropriate media client - EXACTLY like dashboard.py start_metadata_update() + # Get appropriate media client - Support all three servers if active_server == "jellyfin": media_client = jellyfin_client if not media_client: add_activity_item("❌", "Metadata Update", "Jellyfin client not available", "Now") return jsonify({"success": False, "error": "Jellyfin client not available"}), 400 + elif active_server == "navidrome": + media_client = navidrome_client + if not media_client: + add_activity_item("❌", "Metadata Update", "Navidrome client not available", "Now") + return jsonify({"success": False, "error": "Navidrome client not available"}), 400 else: # plex media_client = plex_client if not media_client: @@ -11791,6 +11796,9 @@ class WebMetadataUpdateWorker: # Enable lightweight mode for Jellyfin to skip track caching if self.server_type == "jellyfin": self.media_client.set_metadata_only_mode(True) + elif self.server_type == "navidrome": + # Navidrome doesn't need special mode setting + pass all_artists = self.media_client.get_all_artists() print(f"[DEBUG] Raw artists returned: {[getattr(a, 'title', 'NO_TITLE') for a in (all_artists or [])]}") @@ -11972,27 +11980,37 @@ class WebMetadataUpdateWorker: def update_artist_photo(self, artist, spotify_artist): """Update artist photo from Spotify - EXACT copy from dashboard.py""" try: - # Check if artist already has a good photo - if self.artist_has_valid_photo(artist): + # Check if artist already has a good photo (skip check for Jellyfin) + if self.server_type != "jellyfin" and self.artist_has_valid_photo(artist): + print(f"đŸ–ŧī¸ Skipping {artist.title}: already has valid photo ({getattr(artist, 'thumb', 'None')})") return False - + # Get the image URL from Spotify if not spotify_artist.image_url: + print(f"đŸšĢ Skipping {artist.title}: no Spotify image URL available") return False + + print(f"📸 Processing {artist.title}: downloading from Spotify...") image_url = spotify_artist.image_url # Download and validate image response = requests.get(image_url, timeout=10) response.raise_for_status() + + # Validate and convert image (skip conversion for Jellyfin to preserve format) + if self.server_type == "jellyfin": + # For Jellyfin, use raw image data to preserve original format + image_data = response.content + print(f"📸 Using raw image data for Jellyfin ({len(image_data)} bytes)") + else: + # For other servers, validate and convert + image_data = self.validate_and_convert_image(response.content) + if not image_data: + return False - # Validate and convert image - image_data = self.validate_and_convert_image(response.content) - if not image_data: - return False - - # Upload to media server - return self.upload_artist_poster(artist, image_data) + # Upload to media server using client's method + return self.media_client.update_artist_poster(artist, image_data) except Exception as e: print(f"Error updating photo for {getattr(artist, 'title', 'Unknown')}: {e}") @@ -12216,8 +12234,35 @@ class WebMetadataUpdateWorker: # Refresh artist to see changes artist.refresh() return True + + # Jellyfin: Use Jellyfin API to upload artist image + elif self.server_type == "jellyfin": + import requests + jellyfin_config = config_manager.get_jellyfin_config() + jellyfin_base_url = jellyfin_config.get('base_url', '') + jellyfin_token = jellyfin_config.get('api_key', '') + + if not jellyfin_base_url or not jellyfin_token: + print("❌ Jellyfin configuration missing for image upload") + return False + + upload_url = f"{jellyfin_base_url.rstrip('/')}/Items/{artist.ratingKey}/Images/Primary" + headers = { + 'Authorization': f'MediaBrowser Token="{jellyfin_token}"', + 'Content-Type': 'image/jpeg' + } + + response = requests.post(upload_url, data=image_data, headers=headers) + response.raise_for_status() + return True + + # Navidrome: Currently not supported (Subsonic API doesn't support image uploads) + elif self.server_type == "navidrome": + print("â„šī¸ Navidrome does not support artist image uploads via Subsonic API") + return False + else: - # For other server types, return False since we only have fallback for Plex + # Unknown server type return False except Exception as e: diff --git a/webui/index.html b/webui/index.html index f7530def..40c81bb6 100644 --- a/webui/index.html +++ b/webui/index.html @@ -277,8 +277,10 @@
-

Metadata Updater

-

Updates artist photos, genres, and album art from Spotify.

+
+

Metadata Updater

+
+

Updates artist photos, genres, and album art from Spotify.