Set playlist poster image on Plex/Jellyfin/Emby after sync

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.
pull/273/head
Broque Thomas 2 weeks ago
parent 1e69d813e6
commit 963a003ca0

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

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

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

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

Loading…
Cancel
Save