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

@ -1194,7 +1194,49 @@ class JellyfinClient:
stats['bulk_tracks_cached'] = len(self._all_tracks_cache)
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]:
"""Get all playlists from Jellyfin server"""
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):
"""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.
Supports discover, mirrored, and auto-mirror playlists.
Currently only Navidrome is supported; Plex/Jellyfin users get 'skipped'."""
Supports Navidrome, Plex, and Jellyfin."""
database = get_database()
try:
# Only Navidrome supports playlist push right now
# Resolve the active media server client
active_server = config_manager.get_active_media_server()
if active_server != 'navidrome' or not navidrome_client or not navidrome_client.is_connected():
logger.info(f"[PlaylistPush] Server push skipped — active server is '{active_server}', not navidrome (or not connected)")
server_client = None
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')
return
@ -32749,53 +32756,43 @@ def _push_playlist_to_server(batch_id, batch):
database.update_sync_history_push_status(batch_id, 'skipped')
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
if navidrome_client and navidrome_client.is_connected():
navidrome_client.trigger_library_scan()
elif hasattr(web_scan_manager, 'request_scan'):
web_scan_manager.request_scan(f"Playlist push: {playlist_name}")
server_client.trigger_library_scan()
# Wait for scan to finish (poll every 5s, up to 90s)
if navidrome_client and navidrome_client.is_connected():
for _ in range(18):
time.sleep(5)
if not navidrome_client.is_library_scanning():
break
logger.info(f"[PlaylistPush] Scan complete, searching for tracks")
else:
time.sleep(30)
for _ in range(18):
time.sleep(5)
if not server_client.is_library_scanning():
break
logger.info(f"[PlaylistPush] Scan complete, searching for tracks on {active_server}")
# Search for each track on the media server
matched_server_tracks = []
if navidrome_client and navidrome_client.is_connected():
for t in tracks_to_find:
results = navidrome_client.search_tracks(t['title'], t['artist'], limit=5)
if results:
# Use the first result's underlying NavidromeTrack for playlist creation
best = results[0]
nav_track = getattr(best, '_original_navidrome_track', None)
if nav_track:
matched_server_tracks.append(nav_track)
logger.debug(f"[PlaylistPush] Matched: '{t['title']}' by '{t['artist']}'{best.id}")
else:
matched_server_tracks.append(best)
else:
logger.info(f"[PlaylistPush] No match for: '{t['title']}' by '{t['artist']}'")
for t in tracks_to_find:
results = server_client.search_tracks(t['title'], t['artist'], limit=5)
if results:
best = results[0]
# Navidrome/Plex store the original server object for playlist creation
original = getattr(best, '_original_navidrome_track', None) or getattr(best, '_original_plex_track', None)
matched_server_tracks.append(original if original else best)
logger.debug(f"[PlaylistPush] Matched: '{t['title']}' by '{t['artist']}'{getattr(best, 'id', getattr(best, 'ratingKey', '?'))}")
else:
logger.info(f"[PlaylistPush] No match for: '{t['title']}' by '{t['artist']}'")
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')
return
logger.info(f"[PlaylistPush] Pushing {len(matched_server_tracks)}/{len(tracks_to_find)} tracks to '{playlist_name}' on server")
success = navidrome_client.update_playlist(playlist_name, matched_server_tracks)
logger.info(f"[PlaylistPush] Pushing {len(matched_server_tracks)}/{len(tracks_to_find)} tracks to '{playlist_name}' on {active_server}")
success = server_client.update_playlist(playlist_name, matched_server_tracks)
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')
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')
except Exception as e: