From 71a56bf9b7ffa71f7a9691fcc2d4714e6328c1bf Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:25:38 -0700 Subject: [PATCH] Fix playlist cover art not syncing to Plex/Jellyfin from Deezer and other sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pass playlist image_url to _run_sync_task from all source-specific sync start handlers (Deezer, Tidal, Spotify public, YouTube, automation mirror) — previously only the /api/sync/start endpoint passed it - Fix plex_client.set_playlist_image: use uploadPoster(url=) instead of uploadPoster(data=) which is not a valid PlexAPI argument - deezer_client: use picture_xl > picture_big > picture_medium fallback for better cover art resolution - tidal_client: extract image_url in get_playlist() from JSON:API relationships (was only extracted in metadata-only listing) - parse_youtube_playlist: capture playlist thumbnail from yt-dlp result - Add visible logging for image upload attempts and outcomes --- core/deezer_client.py | 2 +- core/plex_client.py | 13 +++++-------- core/tidal_client.py | 11 +++++++++++ web_server.py | 42 ++++++++++++++++++++++++++---------------- 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/core/deezer_client.py b/core/deezer_client.py index 0684cad2..0a4d0eed 100644 --- a/core/deezer_client.py +++ b/core/deezer_client.py @@ -1114,7 +1114,7 @@ class DeezerClient: 'name': data.get('title', ''), 'description': data.get('description', ''), 'track_count': total_tracks, - 'image_url': data.get('picture_medium', ''), + 'image_url': data.get('picture_xl') or data.get('picture_big') or data.get('picture_medium', ''), 'owner': data.get('creator', {}).get('name', ''), 'tracks': tracks, } diff --git a/core/plex_client.py b/core/plex_client.py index 8c68fc74..39e0e9f4 100644 --- a/core/plex_client.py +++ b/core/plex_client.py @@ -480,19 +480,16 @@ class PlexClient: 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.""" + """Set the poster image for a playlist 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 + playlist.uploadPoster(url=image_url) + 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}") + logger.warning(f"Could not set playlist poster for '{playlist_name}': {e}") return False def _find_track(self, title: str, artist: str, album: str) -> Optional[PlexTrack]: diff --git a/core/tidal_client.py b/core/tidal_client.py index 89bee525..2dbeafbe 100644 --- a/core/tidal_client.py +++ b/core/tidal_client.py @@ -1180,6 +1180,17 @@ class TidalClient: public=playlist_attrs.get('accessType', '') == "PUBLIC" ) + # Extract cover image URL from relationships (same logic as get_user_playlists_metadata_only) + try: + relationships = playlist_data.get('relationships', {}) + image_rel = relationships.get('image', {}).get('data', {}) + if image_rel: + image_id = image_rel.get('id', '') + if image_id: + playlist.image_url = f"https://resources.tidal.com/images/{image_id.replace('-', '/')}/640x640.jpg" + except Exception: + pass + logger.info(f"Retrieved Tidal playlist '{playlist.name}' with {len(tracks)} tracks") return playlist diff --git a/web_server.py b/web_server.py index c978a05e..94e5c8c6 100644 --- a/web_server.py +++ b/web_server.py @@ -957,7 +957,7 @@ def _register_automation_handlers(): log_line=f'Starting sync: {len(tracks_json)} tracks', log_type='success') threading.Thread( target=_run_sync_task, - args=(sync_id, pl['name'], tracks_json, auto_id), + args=(sync_id, pl['name'], tracks_json, auto_id, 1, pl.get('image_url', '')), daemon=True, name=f'auto-sync-{playlist_id}' ).start() @@ -17391,7 +17391,8 @@ def parse_youtube_playlist(url): 'tracks': tracks, 'track_count': len(tracks), 'url': url, - 'source': 'youtube' + 'source': 'youtube', + 'image_url': playlist_info.get('thumbnail', '') or '', } print(f"Successfully parsed YouTube playlist: {len(tracks)} tracks extracted") @@ -34410,14 +34411,15 @@ def start_tidal_sync(playlist_id): with sync_lock: sync_states[sync_playlist_id] = {"status": "starting", "progress": {}} - + # Submit sync task - future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id()) + playlist_image_url = getattr(state['playlist'], 'image_url', '') + future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id(), playlist_image_url) active_sync_workers[sync_playlist_id] = future - + print(f"Started Tidal sync for: {playlist_name} ({len(spotify_tracks)} tracks)") return jsonify({"success": True, "sync_playlist_id": sync_playlist_id}) - + except Exception as e: print(f"Error starting Tidal sync: {e}") return jsonify({"error": str(e)}), 500 @@ -35338,7 +35340,8 @@ def start_deezer_sync(playlist_id): sync_states[sync_playlist_id] = {"status": "starting", "progress": {}} # Submit sync task - future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id()) + playlist_image_url = state['playlist'].get('image_url', '') + future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id(), playlist_image_url) active_sync_workers[sync_playlist_id] = future print(f"Started Deezer sync for: {playlist_name} ({len(spotify_tracks)} tracks)") @@ -36175,7 +36178,8 @@ def start_spotify_public_sync(url_hash): sync_states[sync_playlist_id] = {"status": "starting", "progress": {}} # Submit sync task - future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id()) + playlist_image_url = state['playlist'].get('image_url', '') + future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id(), playlist_image_url) active_sync_workers[sync_playlist_id] = future print(f"Started Spotify Public sync for: {playlist_name} ({len(spotify_tracks)} tracks)") @@ -37204,14 +37208,15 @@ def start_youtube_sync(url_hash): with sync_lock: sync_states[sync_playlist_id] = {"status": "starting", "progress": {}} - + # Submit sync task - future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id()) + playlist_image_url = state['playlist'].get('image_url', '') + future = sync_executor.submit(_run_sync_task, sync_playlist_id, sync_data['playlist_name'], spotify_tracks, None, get_current_profile_id(), playlist_image_url) active_sync_workers[sync_playlist_id] = future - + print(f"Started YouTube sync for: {playlist_name} ({len(spotify_tracks)} tracks)") return jsonify({"success": True, "sync_playlist_id": sync_playlist_id}) - + except Exception as e: print(f"Error starting YouTube sync: {e}") return jsonify({"error": str(e)}), 500 @@ -37834,16 +37839,21 @@ 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: + _synced = getattr(result, 'synced_tracks', 0) + print(f"[PLAYLIST IMAGE] image_url={playlist_image_url!r}, synced_tracks={_synced}") + if playlist_image_url and _synced > 0: try: active_server = config_manager.get_active_media_server() + print(f"[PLAYLIST IMAGE] active_server={active_server}") if active_server == 'plex' and plex_client: - plex_client.set_playlist_image(playlist_name, playlist_image_url) + ok = plex_client.set_playlist_image(playlist_name, playlist_image_url) + print(f"[PLAYLIST IMAGE] Plex upload result: {ok}") elif active_server in ('jellyfin', 'emby') and jellyfin_client: - jellyfin_client.set_playlist_image(playlist_name, playlist_image_url) + ok = jellyfin_client.set_playlist_image(playlist_name, playlist_image_url) + print(f"[PLAYLIST IMAGE] Jellyfin upload result: {ok}") # Navidrome doesn't support custom playlist images except Exception as img_err: - print(f"Could not set playlist image: {img_err}") + print(f"[PLAYLIST IMAGE] Exception: {img_err}") # Record sync history completion with per-track data try: