Support Plex and Jellyfin in playlist server push, add Jellyfin search_tracks
This commit is contained in:
parent
c422c30c21
commit
30e3ed621a
2 changed files with 78 additions and 39 deletions
|
|
@ -1194,7 +1194,49 @@ class JellyfinClient:
|
||||||
stats['bulk_tracks_cached'] = len(self._all_tracks_cache)
|
stats['bulk_tracks_cached'] = len(self._all_tracks_cache)
|
||||||
|
|
||||||
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():
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue