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 || '' }) });