From 963a003ca0c590d7ef7fe635984d797d134008d4 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:08:18 -0700 Subject: [PATCH] Set playlist poster image on Plex/Jellyfin/Emby after sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a successful playlist sync, if the source playlist has cover art (Spotify, Tidal, Deezer, etc.), the image is downloaded and uploaded as the playlist poster on the media server. Plex uses uploadPoster(), Jellyfin/Emby uses POST /Items/{id}/Images/Primary. Navidrome skipped (no playlist image API). Failure is silent — sync result unchanged. Automation-triggered syncs and playlists without images are unaffected. --- core/jellyfin_client.py | 31 +++++++++++++++++++++++++++++++ core/plex_client.py | 16 ++++++++++++++++ web_server.py | 19 ++++++++++++++++--- webui/static/script.js | 3 ++- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/core/jellyfin_client.py b/core/jellyfin_client.py index e51d3d56..f46b24b7 100644 --- a/core/jellyfin_client.py +++ b/core/jellyfin_client.py @@ -1496,6 +1496,37 @@ class JellyfinClient: logger.error(f"Error getting tracks for playlist {playlist_id}: {e}") return [] + def set_playlist_image(self, playlist_name: str, image_url: str) -> bool: + """Set the poster image for a playlist by downloading from a URL.""" + if not self.ensure_connection() or not image_url: + return False + try: + playlist = self.get_playlist_by_name(playlist_name) + if not playlist: + return False + playlist_id = playlist.get('Id') or playlist.get('id') + if not playlist_id: + return False + import requests as _req + img_resp = _req.get(image_url, timeout=15) + if img_resp.ok and img_resp.content: + content_type = img_resp.headers.get('Content-Type', 'image/jpeg') + upload_url = f"{self.base_url}/Items/{playlist_id}/Images/Primary" + upload_resp = _req.post( + upload_url, + headers={'X-Emby-Token': self.api_key, 'Content-Type': content_type}, + data=img_resp.content, + timeout=15 + ) + if upload_resp.ok: + logger.info(f"Set playlist poster for '{playlist_name}'") + return True + else: + logger.debug(f"Playlist image upload returned {upload_resp.status_code}") + except Exception as e: + logger.debug(f"Could not set playlist poster for '{playlist_name}': {e}") + return False + def update_playlist(self, playlist_name: str, tracks) -> bool: """Update an existing playlist or create it if it doesn't exist""" if not self.ensure_connection(): diff --git a/core/plex_client.py b/core/plex_client.py index fd3de9ce..b31d2ae8 100644 --- a/core/plex_client.py +++ b/core/plex_client.py @@ -479,6 +479,22 @@ class PlexClient: logger.error(f"Error updating playlist '{playlist_name}': {e}") return False + def set_playlist_image(self, playlist_name: str, image_url: str) -> bool: + """Set the poster image for a playlist by downloading from a URL.""" + if not self.ensure_connection() or not image_url: + return False + try: + playlist = self.server.playlist(playlist_name) + import requests as _req + img_resp = _req.get(image_url, timeout=15) + if img_resp.ok and img_resp.content: + playlist.uploadPoster(data=img_resp.content) + logger.info(f"Set playlist poster for '{playlist_name}'") + return True + except Exception as e: + logger.debug(f"Could not set playlist poster for '{playlist_name}': {e}") + return False + def _find_track(self, title: str, artist: str, album: str) -> Optional[PlexTrack]: if not self.music_library: return None diff --git a/web_server.py b/web_server.py index 163cc1ed..6b4d5356 100644 --- a/web_server.py +++ b/web_server.py @@ -36103,7 +36103,7 @@ def convert_youtube_results_to_spotify_tracks(discovery_results): # Add these new endpoints to the end of web_server.py -def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, profile_id=1): +def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, profile_id=1, playlist_image_url=''): """The actual sync function that runs in the background thread.""" global sync_states, sync_service @@ -36438,6 +36438,18 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json, automation_id=None, } print(f"🏁 Sync finished for {playlist_id} - state updated") + # Set playlist poster image if available (Plex, Jellyfin, Emby) + if playlist_image_url and getattr(result, 'synced_tracks', 0) > 0: + try: + active_server = config_manager.get_active_media_server() + if active_server == 'plex' and plex_client: + plex_client.set_playlist_image(playlist_name, playlist_image_url) + elif active_server in ('jellyfin', 'emby') and jellyfin_client: + jellyfin_client.set_playlist_image(playlist_name, playlist_image_url) + # Navidrome doesn't support custom playlist images + except Exception as img_err: + print(f"⚠️ Could not set playlist image: {img_err}") + # Record sync history completion with per-track data try: matched = getattr(result, 'matched_tracks', 0) @@ -36536,10 +36548,11 @@ def start_playlist_sync(): playlist_id = data.get('playlist_id') playlist_name = data.get('playlist_name') tracks_json = data.get('tracks') # Pass the full track list + playlist_image_url = data.get('image_url', '') if not all([playlist_id, playlist_name, tracks_json]): return jsonify({"success": False, "error": "Missing playlist_id, name, or tracks."}), 400 - + # Add activity for sync start add_activity_item("🔄", "Spotify Sync Started", f"'{playlist_name}' - {len(tracks_json)} tracks", "Now") @@ -36556,7 +36569,7 @@ def start_playlist_sync(): # Submit the task to the thread pool (capture profile_id while still in request context) _sync_profile_id = get_current_profile_id() thread_submit_time = time.time() - future = sync_executor.submit(_run_sync_task, playlist_id, playlist_name, tracks_json, None, _sync_profile_id) + future = sync_executor.submit(_run_sync_task, playlist_id, playlist_name, tracks_json, None, _sync_profile_id, playlist_image_url) active_sync_workers[playlist_id] = future thread_submit_duration = (time.time() - thread_submit_time) * 1000 print(f"⏱️ [TIMING] Thread submitted at {time.strftime('%H:%M:%S')} (took {thread_submit_duration:.1f}ms)") diff --git a/webui/static/script.js b/webui/static/script.js index 3eb63314..2ceec24c 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -15983,7 +15983,8 @@ async function startPlaylistSync(playlistId) { body: JSON.stringify({ playlist_id: playlist.id, playlist_name: playlist.name, - tracks: tracks // Send the full track list + tracks: tracks, // Send the full track list + image_url: playlist.image_url || '' }) });