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.
This commit is contained in:
parent
1e69d813e6
commit
963a003ca0
4 changed files with 65 additions and 4 deletions
|
|
@ -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…
Reference in a new issue