navidrome support

This commit is contained in:
Broque Thomas 2025-09-22 07:49:48 -07:00
parent 907ea21103
commit 41df3d84b7
2 changed files with 71 additions and 33 deletions

View file

@ -6,6 +6,7 @@ from utils.logging_config import get_logger
from core.spotify_client import SpotifyClient, Playlist as SpotifyPlaylist, Track as SpotifyTrack
from core.plex_client import PlexClient, PlexTrackInfo
from core.jellyfin_client import JellyfinClient
from core.navidrome_client import NavidromeClient
from core.soulseek_client import SoulseekClient
from core.matching_engine import MusicMatchingEngine, MatchResult
@ -41,10 +42,11 @@ class SyncProgress:
failed_tracks: int = 0
class PlaylistSyncService:
def __init__(self, spotify_client: SpotifyClient, plex_client: PlexClient, soulseek_client: SoulseekClient, jellyfin_client: JellyfinClient = None):
def __init__(self, spotify_client: SpotifyClient, plex_client: PlexClient, soulseek_client: SoulseekClient, jellyfin_client: JellyfinClient = None, navidrome_client = None):
self.spotify_client = spotify_client
self.plex_client = plex_client
self.jellyfin_client = jellyfin_client
self.navidrome_client = navidrome_client
self.soulseek_client = soulseek_client
self.progress_callbacks = {} # Playlist-specific progress callbacks
self.syncing_playlists = set() # Track multiple syncing playlists
@ -56,12 +58,17 @@ class PlaylistSyncService:
try:
from config.settings import config_manager
active_server = config_manager.get_active_media_server()
if active_server == "jellyfin":
if not self.jellyfin_client:
logger.error("Jellyfin client not provided to sync service")
return None, "jellyfin"
return self.jellyfin_client, "jellyfin"
elif active_server == "navidrome":
if not self.navidrome_client:
logger.error("Navidrome client not provided to sync service")
return None, "navidrome"
return self.navidrome_client, "navidrome"
else: # Default to Plex
return self.plex_client, "plex"
except Exception as e:
@ -148,7 +155,8 @@ class PlaylistSyncService:
total_tracks = len(playlist.tracks)
media_client, server_type = self._get_active_media_client()
self._update_progress(playlist.name, "Matching tracks against Plex library", "", 20, 5, 2, total_tracks=total_tracks)
media_client, server_type = self._get_active_media_client()
self._update_progress(playlist.name, f"Matching tracks against {server_type.title()} library", "", 20, 5, 2, total_tracks=total_tracks)
# Use the same robust matching approach as "Download Missing Tracks"
match_results = []
@ -203,7 +211,8 @@ class PlaylistSyncService:
if self._cancelled:
return self._create_error_result(playlist.name, ["Sync cancelled"])
self._update_progress(playlist.name, "Creating/updating Plex playlist", "", 80, 5, 4,
media_client, server_type = self._get_active_media_client()
self._update_progress(playlist.name, f"Creating/updating {server_type.title()} playlist", "", 80, 5, 4,
total_tracks=total_tracks,
matched_tracks=len(matched_tracks),
failed_tracks=len(unmatched_tracks))
@ -313,6 +322,17 @@ class PlaylistSyncService:
actual_track = JellyfinTrackFromDB(db_track)
logger.debug(f"✔️ Created Jellyfin track object for '{db_track.title}' (ID: {actual_track.ratingKey})")
return actual_track, confidence
elif server_type == "navidrome":
# For Navidrome, create a track object from database info (similar to Jellyfin)
class NavidromeTrackFromDB:
def __init__(self, db_track):
self.ratingKey = db_track.id
self.title = db_track.title
self.id = db_track.id
actual_track = NavidromeTrackFromDB(db_track)
logger.debug(f"✔️ Created Navidrome track object for '{db_track.title}' (ID: {actual_track.ratingKey})")
return actual_track, confidence
else:
# For Plex, use the original fetchItem approach
# Validate that the track ID is numeric (Plex requirement)
@ -368,11 +388,21 @@ class PlaylistSyncService:
logger.error(f"Error fetching Spotify playlist: {e}")
return None
async def _get_plex_tracks(self) -> List[PlexTrackInfo]:
async def _get_media_tracks(self) -> List:
"""Get tracks from the active media server"""
try:
return self.plex_client.search_tracks("", limit=10000)
media_client, server_type = self._get_active_media_client()
if not media_client:
logger.error(f"No active media client available")
return []
if hasattr(media_client, 'search_tracks'):
return media_client.search_tracks("", limit=10000)
else:
logger.warning(f"{server_type.title()} client doesn't support track search")
return []
except Exception as e:
logger.error(f"Error fetching Plex tracks: {e}")
logger.error(f"Error fetching {server_type} tracks: {e}")
return []
async def _download_missing_tracks(self, unmatched_tracks: List[MatchResult]) -> int:
@ -415,37 +445,41 @@ class PlaylistSyncService:
spotify_playlist = self._get_spotify_playlist(playlist_name)
if not spotify_playlist:
return {"error": f"Playlist '{playlist_name}' not found"}
plex_tracks = self.plex_client.search_tracks("", limit=1000)
media_client, server_type = self._get_active_media_client()
if not media_client or not hasattr(media_client, 'search_tracks'):
return {"error": f"Active media server ({server_type}) doesn't support track search"}
media_tracks = media_client.search_tracks("", limit=1000)
match_results = self.matching_engine.match_playlist_tracks(
spotify_playlist.tracks,
plex_tracks
spotify_playlist.tracks,
media_tracks
)
stats = self.matching_engine.get_match_statistics(match_results)
preview = {
"playlist_name": playlist_name,
"total_tracks": len(spotify_playlist.tracks),
"available_in_plex": stats["matched_tracks"],
f"available_in_{server_type}": stats["matched_tracks"],
"needs_download": stats["total_tracks"] - stats["matched_tracks"],
"match_percentage": stats["match_percentage"],
"confidence_breakdown": stats["confidence_distribution"],
"tracks_preview": []
}
for result in match_results[:10]:
track_info = {
"spotify_track": f"{result.spotify_track.name} - {result.spotify_track.artists[0]}",
"plex_match": result.plex_track.title if result.plex_track else None,
f"{server_type}_match": getattr(result, 'plex_track', None).title if getattr(result, 'plex_track', None) else None,
"confidence": result.confidence,
"status": "available" if result.is_match else "needs_download"
}
preview["tracks_preview"].append(track_info)
return preview
except Exception as e:
logger.error(f"Error generating sync preview: {e}")
return {"error": str(e)}
@ -453,30 +487,34 @@ class PlaylistSyncService:
def get_library_comparison(self) -> Dict[str, Any]:
try:
spotify_playlists = self.spotify_client.get_user_playlists()
plex_playlists = self.plex_client.get_all_playlists()
plex_stats = self.plex_client.get_library_stats()
spotify_track_count = sum(len(p.tracks) for p in spotify_playlists)
media_client, server_type = self._get_active_media_client()
if not media_client:
return {"error": f"No active media client available"}
media_playlists = media_client.get_all_playlists() if hasattr(media_client, 'get_all_playlists') else []
media_stats = media_client.get_library_stats() if hasattr(media_client, 'get_library_stats') else {}
comparison = {
"spotify": {
"playlists": len(spotify_playlists),
"total_tracks": spotify_track_count
},
"plex": {
"playlists": len(plex_playlists),
"artists": plex_stats.get("artists", 0),
"albums": plex_stats.get("albums", 0),
"tracks": plex_stats.get("tracks", 0)
server_type: {
"playlists": len(media_playlists),
"artists": media_stats.get("artists", 0),
"albums": media_stats.get("albums", 0),
"tracks": media_stats.get("tracks", 0)
},
"sync_potential": {
"estimated_matches": min(spotify_track_count, plex_stats.get("tracks", 0)),
"potential_downloads": max(0, spotify_track_count - plex_stats.get("tracks", 0))
"estimated_matches": min(spotify_track_count, media_stats.get("tracks", 0)),
"potential_downloads": max(0, spotify_track_count - media_stats.get("tracks", 0))
}
}
return comparison
except Exception as e:
logger.error(f"Error generating library comparison: {e}")
return {"error": str(e)}

View file

@ -100,7 +100,7 @@ try:
soulseek_client = SoulseekClient()
tidal_client = TidalClient()
matching_engine = MusicMatchingEngine()
sync_service = PlaylistSyncService(spotify_client, plex_client, soulseek_client, jellyfin_client)
sync_service = PlaylistSyncService(spotify_client, plex_client, soulseek_client, jellyfin_client, navidrome_client)
print("✅ Core service clients initialized.")
except Exception as e:
print(f"🔴 FATAL: Error initializing service clients: {e}")