navidrome support
This commit is contained in:
parent
907ea21103
commit
41df3d84b7
2 changed files with 71 additions and 33 deletions
|
|
@ -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.spotify_client import SpotifyClient, Playlist as SpotifyPlaylist, Track as SpotifyTrack
|
||||||
from core.plex_client import PlexClient, PlexTrackInfo
|
from core.plex_client import PlexClient, PlexTrackInfo
|
||||||
from core.jellyfin_client import JellyfinClient
|
from core.jellyfin_client import JellyfinClient
|
||||||
|
from core.navidrome_client import NavidromeClient
|
||||||
from core.soulseek_client import SoulseekClient
|
from core.soulseek_client import SoulseekClient
|
||||||
from core.matching_engine import MusicMatchingEngine, MatchResult
|
from core.matching_engine import MusicMatchingEngine, MatchResult
|
||||||
|
|
||||||
|
|
@ -41,10 +42,11 @@ class SyncProgress:
|
||||||
failed_tracks: int = 0
|
failed_tracks: int = 0
|
||||||
|
|
||||||
class PlaylistSyncService:
|
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.spotify_client = spotify_client
|
||||||
self.plex_client = plex_client
|
self.plex_client = plex_client
|
||||||
self.jellyfin_client = jellyfin_client
|
self.jellyfin_client = jellyfin_client
|
||||||
|
self.navidrome_client = navidrome_client
|
||||||
self.soulseek_client = soulseek_client
|
self.soulseek_client = soulseek_client
|
||||||
self.progress_callbacks = {} # Playlist-specific progress callbacks
|
self.progress_callbacks = {} # Playlist-specific progress callbacks
|
||||||
self.syncing_playlists = set() # Track multiple syncing playlists
|
self.syncing_playlists = set() # Track multiple syncing playlists
|
||||||
|
|
@ -62,6 +64,11 @@ class PlaylistSyncService:
|
||||||
logger.error("Jellyfin client not provided to sync service")
|
logger.error("Jellyfin client not provided to sync service")
|
||||||
return None, "jellyfin"
|
return None, "jellyfin"
|
||||||
return self.jellyfin_client, "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
|
else: # Default to Plex
|
||||||
return self.plex_client, "plex"
|
return self.plex_client, "plex"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -148,7 +155,8 @@ class PlaylistSyncService:
|
||||||
total_tracks = len(playlist.tracks)
|
total_tracks = len(playlist.tracks)
|
||||||
media_client, server_type = self._get_active_media_client()
|
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"
|
# Use the same robust matching approach as "Download Missing Tracks"
|
||||||
match_results = []
|
match_results = []
|
||||||
|
|
@ -203,7 +211,8 @@ class PlaylistSyncService:
|
||||||
if self._cancelled:
|
if self._cancelled:
|
||||||
return self._create_error_result(playlist.name, ["Sync 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,
|
total_tracks=total_tracks,
|
||||||
matched_tracks=len(matched_tracks),
|
matched_tracks=len(matched_tracks),
|
||||||
failed_tracks=len(unmatched_tracks))
|
failed_tracks=len(unmatched_tracks))
|
||||||
|
|
@ -313,6 +322,17 @@ class PlaylistSyncService:
|
||||||
actual_track = JellyfinTrackFromDB(db_track)
|
actual_track = JellyfinTrackFromDB(db_track)
|
||||||
logger.debug(f"✔️ Created Jellyfin track object for '{db_track.title}' (ID: {actual_track.ratingKey})")
|
logger.debug(f"✔️ Created Jellyfin track object for '{db_track.title}' (ID: {actual_track.ratingKey})")
|
||||||
return actual_track, confidence
|
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:
|
else:
|
||||||
# For Plex, use the original fetchItem approach
|
# For Plex, use the original fetchItem approach
|
||||||
# Validate that the track ID is numeric (Plex requirement)
|
# Validate that the track ID is numeric (Plex requirement)
|
||||||
|
|
@ -368,11 +388,21 @@ class PlaylistSyncService:
|
||||||
logger.error(f"Error fetching Spotify playlist: {e}")
|
logger.error(f"Error fetching Spotify playlist: {e}")
|
||||||
return None
|
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:
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching Plex tracks: {e}")
|
logger.error(f"Error fetching {server_type} tracks: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def _download_missing_tracks(self, unmatched_tracks: List[MatchResult]) -> int:
|
async def _download_missing_tracks(self, unmatched_tracks: List[MatchResult]) -> int:
|
||||||
|
|
@ -416,11 +446,15 @@ class PlaylistSyncService:
|
||||||
if not spotify_playlist:
|
if not spotify_playlist:
|
||||||
return {"error": f"Playlist '{playlist_name}' not found"}
|
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(
|
match_results = self.matching_engine.match_playlist_tracks(
|
||||||
spotify_playlist.tracks,
|
spotify_playlist.tracks,
|
||||||
plex_tracks
|
media_tracks
|
||||||
)
|
)
|
||||||
|
|
||||||
stats = self.matching_engine.get_match_statistics(match_results)
|
stats = self.matching_engine.get_match_statistics(match_results)
|
||||||
|
|
@ -428,7 +462,7 @@ class PlaylistSyncService:
|
||||||
preview = {
|
preview = {
|
||||||
"playlist_name": playlist_name,
|
"playlist_name": playlist_name,
|
||||||
"total_tracks": len(spotify_playlist.tracks),
|
"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"],
|
"needs_download": stats["total_tracks"] - stats["matched_tracks"],
|
||||||
"match_percentage": stats["match_percentage"],
|
"match_percentage": stats["match_percentage"],
|
||||||
"confidence_breakdown": stats["confidence_distribution"],
|
"confidence_breakdown": stats["confidence_distribution"],
|
||||||
|
|
@ -438,7 +472,7 @@ class PlaylistSyncService:
|
||||||
for result in match_results[:10]:
|
for result in match_results[:10]:
|
||||||
track_info = {
|
track_info = {
|
||||||
"spotify_track": f"{result.spotify_track.name} - {result.spotify_track.artists[0]}",
|
"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,
|
"confidence": result.confidence,
|
||||||
"status": "available" if result.is_match else "needs_download"
|
"status": "available" if result.is_match else "needs_download"
|
||||||
}
|
}
|
||||||
|
|
@ -453,25 +487,29 @@ class PlaylistSyncService:
|
||||||
def get_library_comparison(self) -> Dict[str, Any]:
|
def get_library_comparison(self) -> Dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
spotify_playlists = self.spotify_client.get_user_playlists()
|
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)
|
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 = {
|
comparison = {
|
||||||
"spotify": {
|
"spotify": {
|
||||||
"playlists": len(spotify_playlists),
|
"playlists": len(spotify_playlists),
|
||||||
"total_tracks": spotify_track_count
|
"total_tracks": spotify_track_count
|
||||||
},
|
},
|
||||||
"plex": {
|
server_type: {
|
||||||
"playlists": len(plex_playlists),
|
"playlists": len(media_playlists),
|
||||||
"artists": plex_stats.get("artists", 0),
|
"artists": media_stats.get("artists", 0),
|
||||||
"albums": plex_stats.get("albums", 0),
|
"albums": media_stats.get("albums", 0),
|
||||||
"tracks": plex_stats.get("tracks", 0)
|
"tracks": media_stats.get("tracks", 0)
|
||||||
},
|
},
|
||||||
"sync_potential": {
|
"sync_potential": {
|
||||||
"estimated_matches": min(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 - plex_stats.get("tracks", 0))
|
"potential_downloads": max(0, spotify_track_count - media_stats.get("tracks", 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ try:
|
||||||
soulseek_client = SoulseekClient()
|
soulseek_client = SoulseekClient()
|
||||||
tidal_client = TidalClient()
|
tidal_client = TidalClient()
|
||||||
matching_engine = MusicMatchingEngine()
|
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.")
|
print("✅ Core service clients initialized.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"🔴 FATAL: Error initializing service clients: {e}")
|
print(f"🔴 FATAL: Error initializing service clients: {e}")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue