Fix playlist cover art not syncing to Plex/Jellyfin from Deezer and other sources

- 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
This commit is contained in:
Broque Thomas 2026-04-15 10:25:38 -07:00
parent fe399636b2
commit 71a56bf9b7
4 changed files with 43 additions and 25 deletions

View file

@ -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,
}

View file

@ -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]:

View file

@ -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

View file

@ -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: