Support Plex and Jellyfin in playlist server push, add Jellyfin search_tracks

This commit is contained in:
JohnBaumb 2026-04-23 10:33:50 -07:00
parent c422c30c21
commit 30e3ed621a
2 changed files with 78 additions and 39 deletions

View file

@ -1195,6 +1195,48 @@ class JellyfinClient:
return stats return stats
def search_tracks(self, title: str, artist: str, limit: int = 15) -> List[JellyfinTrack]:
"""Search for tracks by title and artist on the Jellyfin server."""
if not self.ensure_connection():
return []
try:
search_term = f"{artist} {title}".strip()
params = {
'ParentId': self.music_library_id,
'IncludeItemTypes': 'Audio',
'Recursive': True,
'SearchTerm': search_term,
'Fields': 'AlbumId,ArtistItems,Path,MediaSources',
'Limit': limit,
}
response = self._make_request(f'/Users/{self.user_id}/Items', params)
if not response:
return []
results = []
lower_title = title.lower()
lower_artist = artist.lower()
for item in response.get('Items', []):
track = JellyfinTrack(item, self)
# Basic relevance filter: title should appear in track name
if lower_title and lower_title not in track.title.lower():
continue
# If artist provided, check artist names
if lower_artist:
artist_names = [a.get('Name', '').lower() for a in item.get('ArtistItems', [])]
album_artist = (item.get('AlbumArtist') or '').lower()
if not any(lower_artist in n for n in artist_names) and lower_artist not in album_artist:
continue
results.append(track)
return results[:limit]
except Exception as e:
logger.error(f"Error searching Jellyfin tracks for '{title}' by '{artist}': {e}")
return []
def get_all_playlists(self) -> List[JellyfinPlaylistInfo]: def get_all_playlists(self) -> List[JellyfinPlaylistInfo]:
"""Get all playlists from Jellyfin server""" """Get all playlists from Jellyfin server"""
if not self.ensure_connection(): if not self.ensure_connection():

View file

@ -32689,16 +32689,23 @@ def _record_sync_history_completion(batch_id, batch):
def _push_playlist_to_server(batch_id, batch): def _push_playlist_to_server(batch_id, batch):
"""After a batch completes, push the playlist to the media server. """After a batch completes, push the playlist to the active media server.
Runs in a background thread. Triggers a scan, waits, then searches for tracks and creates/updates the playlist. Runs in a background thread. Triggers a scan, waits, then searches for tracks and creates/updates the playlist.
Supports discover, mirrored, and auto-mirror playlists. Supports Navidrome, Plex, and Jellyfin."""
Currently only Navidrome is supported; Plex/Jellyfin users get 'skipped'."""
database = get_database() database = get_database()
try: try:
# Only Navidrome supports playlist push right now # Resolve the active media server client
active_server = config_manager.get_active_media_server() active_server = config_manager.get_active_media_server()
if active_server != 'navidrome' or not navidrome_client or not navidrome_client.is_connected(): server_client = None
logger.info(f"[PlaylistPush] Server push skipped — active server is '{active_server}', not navidrome (or not connected)") if active_server == 'navidrome' and navidrome_client and navidrome_client.is_connected():
server_client = navidrome_client
elif active_server == 'plex' and plex_client and plex_client.is_connected():
server_client = plex_client
elif active_server == 'jellyfin' and jellyfin_client and jellyfin_client.is_connected():
server_client = jellyfin_client
if not server_client:
logger.info(f"[PlaylistPush] Server push skipped — no connected media server (active: '{active_server}')")
database.update_sync_history_push_status(batch_id, 'skipped') database.update_sync_history_push_status(batch_id, 'skipped')
return return
@ -32749,53 +32756,43 @@ def _push_playlist_to_server(batch_id, batch):
database.update_sync_history_push_status(batch_id, 'skipped') database.update_sync_history_push_status(batch_id, 'skipped')
return return
logger.info(f"[PlaylistPush] {playlist_name}: {len(tracks_to_find)} tracks to push to server, triggering scan first") logger.info(f"[PlaylistPush] {playlist_name}: {len(tracks_to_find)} tracks to push to {active_server}, triggering scan first")
# Trigger a library scan so newly downloaded tracks are indexed # Trigger a library scan so newly downloaded tracks are indexed
if navidrome_client and navidrome_client.is_connected(): server_client.trigger_library_scan()
navidrome_client.trigger_library_scan()
elif hasattr(web_scan_manager, 'request_scan'):
web_scan_manager.request_scan(f"Playlist push: {playlist_name}")
# Wait for scan to finish (poll every 5s, up to 90s) # Wait for scan to finish (poll every 5s, up to 90s)
if navidrome_client and navidrome_client.is_connected(): for _ in range(18):
for _ in range(18): time.sleep(5)
time.sleep(5) if not server_client.is_library_scanning():
if not navidrome_client.is_library_scanning(): break
break logger.info(f"[PlaylistPush] Scan complete, searching for tracks on {active_server}")
logger.info(f"[PlaylistPush] Scan complete, searching for tracks")
else:
time.sleep(30)
# Search for each track on the media server # Search for each track on the media server
matched_server_tracks = [] matched_server_tracks = []
if navidrome_client and navidrome_client.is_connected(): for t in tracks_to_find:
for t in tracks_to_find: results = server_client.search_tracks(t['title'], t['artist'], limit=5)
results = navidrome_client.search_tracks(t['title'], t['artist'], limit=5) if results:
if results: best = results[0]
# Use the first result's underlying NavidromeTrack for playlist creation # Navidrome/Plex store the original server object for playlist creation
best = results[0] original = getattr(best, '_original_navidrome_track', None) or getattr(best, '_original_plex_track', None)
nav_track = getattr(best, '_original_navidrome_track', None) matched_server_tracks.append(original if original else best)
if nav_track: logger.debug(f"[PlaylistPush] Matched: '{t['title']}' by '{t['artist']}'{getattr(best, 'id', getattr(best, 'ratingKey', '?'))}")
matched_server_tracks.append(nav_track) else:
logger.debug(f"[PlaylistPush] Matched: '{t['title']}' by '{t['artist']}'{best.id}") logger.info(f"[PlaylistPush] No match for: '{t['title']}' by '{t['artist']}'")
else:
matched_server_tracks.append(best)
else:
logger.info(f"[PlaylistPush] No match for: '{t['title']}' by '{t['artist']}'")
if not matched_server_tracks: if not matched_server_tracks:
logger.warning(f"[PlaylistPush] No tracks matched on server for {playlist_name}") logger.warning(f"[PlaylistPush] No tracks matched on {active_server} for {playlist_name}")
database.update_sync_history_push_status(batch_id, 'failed') database.update_sync_history_push_status(batch_id, 'failed')
return return
logger.info(f"[PlaylistPush] Pushing {len(matched_server_tracks)}/{len(tracks_to_find)} tracks to '{playlist_name}' on server") logger.info(f"[PlaylistPush] Pushing {len(matched_server_tracks)}/{len(tracks_to_find)} tracks to '{playlist_name}' on {active_server}")
success = navidrome_client.update_playlist(playlist_name, matched_server_tracks) success = server_client.update_playlist(playlist_name, matched_server_tracks)
if success: if success:
logger.info(f"[PlaylistPush] Successfully pushed '{playlist_name}' to server with {len(matched_server_tracks)} tracks") logger.info(f"[PlaylistPush] Successfully pushed '{playlist_name}' to {active_server} with {len(matched_server_tracks)} tracks")
database.update_sync_history_push_status(batch_id, 'success') database.update_sync_history_push_status(batch_id, 'success')
else: else:
logger.warning(f"[PlaylistPush] Failed to push '{playlist_name}' to server") logger.warning(f"[PlaylistPush] Failed to push '{playlist_name}' to {active_server}")
database.update_sync_history_push_status(batch_id, 'failed') database.update_sync_history_push_status(batch_id, 'failed')
except Exception as e: except Exception as e: