MS Gap 1: Lift shared TrackInfo + PlaylistInfo to neutral types module
Plex / Jellyfin / Navidrome each defined a near-identical XTrackInfo (id / title / artist / album / duration / track_number / year / rating) and XPlaylistInfo (id / title / description / duration / leaf_count / tracks). Three classes that grew up by copy-paste — not a real contract difference. Lifted both to core/media_server/types.py as canonical TrackInfo + PlaylistInfo. Per-server constructors live as classmethods on the unified class (TrackInfo.from_plex_track, PlaylistInfo.from_plex_playlist) matching the metadata Album.from_X_dict pattern Cin's POC uses. Heavy plexapi imports stay lazy under TYPE_CHECKING. - core/plex_client.py / jellyfin_client.py / navidrome_client.py: per-server XTrackInfo / XPlaylistInfo dataclass definitions removed; each module now imports TrackInfo + PlaylistInfo from the neutral package and uses the shared name internally. - core/matching_engine.py: was annotating callers with PlexTrackInfo even though sync_service hands it Jellyfin / Navidrome instances at runtime when those servers are active. Annotation is now the unified TrackInfo, so signatures match the actual contract. - services/sync_service.py: same import + annotation update.
This commit is contained in:
parent
a6bb5f5b43
commit
2ebaf2e6e3
6 changed files with 177 additions and 130 deletions
|
|
@ -1,33 +1,19 @@
|
|||
import requests
|
||||
import time
|
||||
from typing import List, Optional, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import json
|
||||
from utils.logging_config import get_logger
|
||||
from config.settings import config_manager
|
||||
|
||||
# Shared dataclasses live in the neutral media_server package — every
|
||||
# server client used to define a near-identical XTrackInfo /
|
||||
# XPlaylistInfo. Lifted to one canonical type so consumers (matching
|
||||
# engine, sync service) get a single import.
|
||||
from core.media_server.types import TrackInfo, PlaylistInfo
|
||||
|
||||
logger = get_logger("jellyfin_client")
|
||||
|
||||
@dataclass
|
||||
class JellyfinTrackInfo:
|
||||
id: str
|
||||
title: str
|
||||
artist: str
|
||||
album: str
|
||||
duration: int
|
||||
track_number: Optional[int] = None
|
||||
year: Optional[int] = None
|
||||
rating: Optional[float] = None
|
||||
|
||||
@dataclass
|
||||
class JellyfinPlaylistInfo:
|
||||
id: str
|
||||
title: str
|
||||
description: Optional[str]
|
||||
duration: int
|
||||
leaf_count: int
|
||||
tracks: List[JellyfinTrackInfo]
|
||||
|
||||
class JellyfinArtist:
|
||||
"""Wrapper class to mimic Plex artist object interface"""
|
||||
|
|
@ -1204,7 +1190,7 @@ class JellyfinClient(MediaServerClient):
|
|||
|
||||
return stats
|
||||
|
||||
def get_all_playlists(self) -> List[JellyfinPlaylistInfo]:
|
||||
def get_all_playlists(self) -> List[PlaylistInfo]:
|
||||
"""Get all playlists from Jellyfin server"""
|
||||
if not self.ensure_connection():
|
||||
return []
|
||||
|
|
@ -1221,7 +1207,7 @@ class JellyfinClient(MediaServerClient):
|
|||
|
||||
playlists = []
|
||||
for item in response.get('Items', []):
|
||||
playlist_info = JellyfinPlaylistInfo(
|
||||
playlist_info = PlaylistInfo(
|
||||
id=item.get('Id', ''),
|
||||
title=item.get('Name', 'Unknown Playlist'),
|
||||
description=item.get('Overview'),
|
||||
|
|
@ -1238,7 +1224,7 @@ class JellyfinClient(MediaServerClient):
|
|||
logger.error(f"Error getting playlists from Jellyfin: {e}")
|
||||
return []
|
||||
|
||||
def get_playlist_by_name(self, name: str) -> Optional[JellyfinPlaylistInfo]:
|
||||
def get_playlist_by_name(self, name: str) -> Optional[PlaylistInfo]:
|
||||
"""Get a specific playlist by name"""
|
||||
playlists = self.get_all_playlists()
|
||||
for playlist in playlists:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from utils.logging_config import get_logger
|
|||
from config.settings import config_manager
|
||||
|
||||
from core.spotify_client import Track as SpotifyTrack
|
||||
from core.plex_client import PlexTrackInfo
|
||||
from core.media_server.types import TrackInfo
|
||||
from core.soulseek_client import TrackResult, AlbumResult
|
||||
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ logger = get_logger("matching_engine")
|
|||
@dataclass
|
||||
class MatchResult:
|
||||
spotify_track: SpotifyTrack
|
||||
plex_track: Optional[PlexTrackInfo]
|
||||
plex_track: Optional[TrackInfo]
|
||||
confidence: float
|
||||
match_type: str
|
||||
|
||||
|
|
@ -302,7 +302,7 @@ class MusicMatchingEngine:
|
|||
confidence = (title_score * 0.60) + (artist_score * 0.30) + (duration_score * 0.10)
|
||||
return confidence, "standard_match"
|
||||
|
||||
def calculate_match_confidence(self, spotify_track: SpotifyTrack, plex_track: PlexTrackInfo) -> Tuple[float, str]:
|
||||
def calculate_match_confidence(self, spotify_track: SpotifyTrack, plex_track: TrackInfo) -> Tuple[float, str]:
|
||||
"""Calculates a confidence score using a prioritized model, starting with a strict 'core' title check."""
|
||||
return self.score_track_match(
|
||||
source_title=spotify_track.name,
|
||||
|
|
@ -313,7 +313,7 @@ class MusicMatchingEngine:
|
|||
candidate_duration_ms=plex_track.duration if plex_track.duration else 0
|
||||
)
|
||||
|
||||
def find_best_match(self, spotify_track: SpotifyTrack, plex_tracks: List[PlexTrackInfo]) -> MatchResult:
|
||||
def find_best_match(self, spotify_track: SpotifyTrack, plex_tracks: List[TrackInfo]) -> MatchResult:
|
||||
"""Finds the best Plex track match from a list of candidates."""
|
||||
best_match = None
|
||||
best_confidence = 0.0
|
||||
|
|
|
|||
130
core/media_server/types.py
Normal file
130
core/media_server/types.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""Shared dataclasses for the media-server contract.
|
||||
|
||||
Plex / Jellyfin / Navidrome / SoulSync clients all surfaced near-
|
||||
identical ``XTrackInfo`` and ``XPlaylistInfo`` shapes (id, title,
|
||||
artist, album, duration, track_number, year, optional rating)
|
||||
because every consumer needed the same shape downstream. The
|
||||
per-server class names were a copy-paste artifact, not a real
|
||||
contract difference.
|
||||
|
||||
This module owns the canonical types. Per-server constructors live
|
||||
on the unified class as classmethods (``TrackInfo.from_plex_track``,
|
||||
``TrackInfo.from_jellyfin_dict``, ``TrackInfo.from_navidrome_dict``)
|
||||
— same pattern Cin used for ``Album.from_spotify_dict`` etc. on the
|
||||
metadata engine. Heavy server SDK types (``PlexTrack``, dict
|
||||
shapes) imported under TYPE_CHECKING so this module stays
|
||||
import-light.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# plexapi types — only loaded when type-checking; runtime
|
||||
# paths through from_plex_track accept whatever PlexClient
|
||||
# passes through.
|
||||
from plexapi.audio import Track as PlexTrack
|
||||
from plexapi.playlist import Playlist as PlexPlaylist
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrackInfo:
|
||||
"""Canonical track-shape returned by every media server client.
|
||||
|
||||
All four servers (Plex, Jellyfin, Navidrome, SoulSync standalone)
|
||||
used to define their own near-identical ``XTrackInfo`` dataclass.
|
||||
Lifted to one canonical type here so consumers (matching engine,
|
||||
sync service, library scanners) get a single import.
|
||||
"""
|
||||
|
||||
id: str
|
||||
title: str
|
||||
artist: str
|
||||
album: str
|
||||
duration: int
|
||||
track_number: Optional[int] = None
|
||||
year: Optional[int] = None
|
||||
rating: Optional[float] = None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Per-server constructors — mirror Cin's metadata Album.from_X_dict
|
||||
# pattern. Each one knows ONE server's wire shape.
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def from_plex_track(cls, track: 'PlexTrack') -> 'TrackInfo':
|
||||
"""Build a TrackInfo from a plexapi PlexTrack.
|
||||
|
||||
Defensive: tracks may be missing artist or album metadata in
|
||||
Plex (especially fan-uploaded content); fall back to
|
||||
"Unknown Artist" / "Unknown Album" instead of raising.
|
||||
"""
|
||||
# Imported lazily so this module stays import-light. plexapi
|
||||
# is heavy and pulls in network + ssl deps just to define
|
||||
# exception types.
|
||||
from plexapi.exceptions import NotFound
|
||||
|
||||
try:
|
||||
artist_title = track.artist().title if track.artist() else "Unknown Artist"
|
||||
except (NotFound, AttributeError):
|
||||
artist_title = "Unknown Artist"
|
||||
|
||||
try:
|
||||
album_title = track.album().title if track.album() else "Unknown Album"
|
||||
except (NotFound, AttributeError):
|
||||
album_title = "Unknown Album"
|
||||
|
||||
return cls(
|
||||
id=str(track.ratingKey),
|
||||
title=track.title,
|
||||
artist=artist_title,
|
||||
album=album_title,
|
||||
duration=track.duration,
|
||||
track_number=track.trackNumber,
|
||||
year=track.year,
|
||||
rating=track.userRating,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlaylistInfo:
|
||||
"""Canonical playlist-shape returned by every media server client.
|
||||
|
||||
Same lift rationale as ``TrackInfo`` — every server defined the
|
||||
same five-field dataclass + a list of tracks.
|
||||
"""
|
||||
|
||||
id: str
|
||||
title: str
|
||||
description: Optional[str]
|
||||
duration: int
|
||||
leaf_count: int
|
||||
tracks: List[TrackInfo] = field(default_factory=list)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Per-server constructors
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def from_plex_playlist(cls, playlist: 'PlexPlaylist') -> 'PlaylistInfo':
|
||||
"""Build a PlaylistInfo from a plexapi Playlist. Skips items
|
||||
that aren't audio tracks (Plex playlists can mix media types
|
||||
in theory, though music libraries shouldn't)."""
|
||||
from plexapi.audio import Track as PlexTrack
|
||||
|
||||
tracks: List[TrackInfo] = []
|
||||
for item in playlist.items():
|
||||
if isinstance(item, PlexTrack):
|
||||
tracks.append(TrackInfo.from_plex_track(item))
|
||||
|
||||
return cls(
|
||||
id=str(playlist.ratingKey),
|
||||
title=playlist.title,
|
||||
description=playlist.summary,
|
||||
duration=playlist.duration,
|
||||
leaf_count=playlist.leafCount,
|
||||
tracks=tracks,
|
||||
)
|
||||
|
|
@ -2,33 +2,19 @@ import requests
|
|||
import hashlib
|
||||
import secrets
|
||||
from typing import List, Optional, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import json
|
||||
from utils.logging_config import get_logger
|
||||
from config.settings import config_manager
|
||||
|
||||
# Shared dataclasses live in the neutral media_server package — every
|
||||
# server client used to define a near-identical XTrackInfo /
|
||||
# XPlaylistInfo. Lifted to one canonical type so consumers (matching
|
||||
# engine, sync service) get a single import.
|
||||
from core.media_server.types import TrackInfo, PlaylistInfo
|
||||
|
||||
logger = get_logger("navidrome_client")
|
||||
|
||||
@dataclass
|
||||
class NavidromeTrackInfo:
|
||||
id: str
|
||||
title: str
|
||||
artist: str
|
||||
album: str
|
||||
duration: int
|
||||
track_number: Optional[int] = None
|
||||
year: Optional[int] = None
|
||||
rating: Optional[float] = None
|
||||
|
||||
@dataclass
|
||||
class NavidromePlaylistInfo:
|
||||
id: str
|
||||
title: str
|
||||
description: Optional[str]
|
||||
duration: int
|
||||
leaf_count: int
|
||||
tracks: List[NavidromeTrackInfo]
|
||||
|
||||
class NavidromeArtist:
|
||||
"""Wrapper class to mimic Plex artist object interface"""
|
||||
|
|
@ -827,7 +813,7 @@ class NavidromeClient(MediaServerClient):
|
|||
return {}
|
||||
return {}
|
||||
|
||||
def get_all_playlists(self) -> List[NavidromePlaylistInfo]:
|
||||
def get_all_playlists(self) -> List[PlaylistInfo]:
|
||||
"""Get all playlists from Navidrome server"""
|
||||
if not self.ensure_connection():
|
||||
return []
|
||||
|
|
@ -841,7 +827,7 @@ class NavidromeClient(MediaServerClient):
|
|||
playlists_data = response.get('playlists', {}).get('playlist', [])
|
||||
|
||||
for playlist_data in playlists_data:
|
||||
playlist_info = NavidromePlaylistInfo(
|
||||
playlist_info = PlaylistInfo(
|
||||
id=playlist_data.get('id', ''),
|
||||
title=playlist_data.get('name', 'Unknown Playlist'),
|
||||
description=playlist_data.get('comment'),
|
||||
|
|
@ -858,7 +844,7 @@ class NavidromeClient(MediaServerClient):
|
|||
logger.error(f"Error getting playlists from Navidrome: {e}")
|
||||
return []
|
||||
|
||||
def get_playlist_by_name(self, name: str) -> Optional[NavidromePlaylistInfo]:
|
||||
def get_playlist_by_name(self, name: str) -> Optional[PlaylistInfo]:
|
||||
"""Get a specific playlist by name"""
|
||||
playlists = self.get_all_playlists()
|
||||
for playlist in playlists:
|
||||
|
|
@ -979,7 +965,7 @@ class NavidromeClient(MediaServerClient):
|
|||
logger.error(f"Error getting tracks for playlist {playlist_id}: {e}")
|
||||
return []
|
||||
|
||||
def get_playlists_by_name(self, name: str) -> List[NavidromePlaylistInfo]:
|
||||
def get_playlists_by_name(self, name: str) -> List[PlaylistInfo]:
|
||||
"""Get all playlists matching a specific name (case-insensitive)"""
|
||||
matches = []
|
||||
playlists = self.get_all_playlists()
|
||||
|
|
@ -1143,7 +1129,7 @@ class NavidromeClient(MediaServerClient):
|
|||
self._track_cache.clear()
|
||||
logger.info("Navidrome client cache cleared")
|
||||
|
||||
def search_tracks(self, title: str, artist: str, limit: int = 15) -> List[NavidromeTrackInfo]:
|
||||
def search_tracks(self, title: str, artist: str, limit: int = 15) -> List[TrackInfo]:
|
||||
"""Search for tracks using Navidrome search API"""
|
||||
if not self.ensure_connection():
|
||||
logger.warning("Navidrome not connected. Cannot perform search.")
|
||||
|
|
@ -1169,7 +1155,7 @@ class NavidromeClient(MediaServerClient):
|
|||
search_result = response.get('searchResult3', {})
|
||||
|
||||
for track_data in search_result.get('song', []):
|
||||
track_info = NavidromeTrackInfo(
|
||||
track_info = TrackInfo(
|
||||
id=track_data.get('id', ''),
|
||||
title=track_data.get('title', ''),
|
||||
artist=track_data.get('artist', ''),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from plexapi.audio import Track as PlexTrack, Album as PlexAlbum, Artist as Plex
|
|||
from plexapi.playlist import Playlist as PlexPlaylist
|
||||
from plexapi.exceptions import PlexApiException, NotFound
|
||||
from typing import List, Optional, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
import requests
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
|
|
@ -12,70 +11,15 @@ from utils.logging_config import get_logger
|
|||
from config.settings import config_manager
|
||||
import threading
|
||||
|
||||
logger = get_logger("plex_client")
|
||||
|
||||
@dataclass
|
||||
class PlexTrackInfo:
|
||||
id: str
|
||||
title: str
|
||||
artist: str
|
||||
album: str
|
||||
duration: int
|
||||
track_number: Optional[int] = None
|
||||
year: Optional[int] = None
|
||||
rating: Optional[float] = None
|
||||
|
||||
@classmethod
|
||||
def from_plex_track(cls, track: PlexTrack) -> 'PlexTrackInfo':
|
||||
# Gracefully handle tracks that might be missing artist or album metadata in Plex
|
||||
try:
|
||||
artist_title = track.artist().title if track.artist() else "Unknown Artist"
|
||||
except (NotFound, AttributeError):
|
||||
artist_title = "Unknown Artist"
|
||||
|
||||
try:
|
||||
album_title = track.album().title if track.album() else "Unknown Album"
|
||||
except (NotFound, AttributeError):
|
||||
album_title = "Unknown Album"
|
||||
|
||||
return cls(
|
||||
id=str(track.ratingKey),
|
||||
title=track.title,
|
||||
artist=artist_title,
|
||||
album=album_title,
|
||||
duration=track.duration,
|
||||
track_number=track.trackNumber,
|
||||
year=track.year,
|
||||
rating=track.userRating
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class PlexPlaylistInfo:
|
||||
id: str
|
||||
title: str
|
||||
description: Optional[str]
|
||||
duration: int
|
||||
leaf_count: int
|
||||
tracks: List[PlexTrackInfo]
|
||||
|
||||
@classmethod
|
||||
def from_plex_playlist(cls, playlist: PlexPlaylist) -> 'PlexPlaylistInfo':
|
||||
tracks = []
|
||||
for item in playlist.items():
|
||||
if isinstance(item, PlexTrack):
|
||||
tracks.append(PlexTrackInfo.from_plex_track(item))
|
||||
|
||||
return cls(
|
||||
id=str(playlist.ratingKey),
|
||||
title=playlist.title,
|
||||
description=playlist.summary,
|
||||
duration=playlist.duration,
|
||||
leaf_count=playlist.leafCount,
|
||||
tracks=tracks
|
||||
)
|
||||
|
||||
# Shared dataclasses live in the neutral media_server package now —
|
||||
# every server client used to define a near-identical XTrackInfo /
|
||||
# XPlaylistInfo. Lifted to one canonical type so consumers (matching
|
||||
# engine, sync service, etc.) get a single import.
|
||||
from core.media_server.types import TrackInfo, PlaylistInfo
|
||||
from core.media_server.contract import MediaServerClient
|
||||
|
||||
logger = get_logger("plex_client")
|
||||
|
||||
|
||||
class PlexClient(MediaServerClient):
|
||||
def __init__(self):
|
||||
|
|
@ -271,7 +215,7 @@ class PlexClient(MediaServerClient):
|
|||
"""Check if both server is connected AND music library is selected."""
|
||||
return self.server is not None and self.music_library is not None
|
||||
|
||||
def get_all_playlists(self) -> List[PlexPlaylistInfo]:
|
||||
def get_all_playlists(self) -> List[PlaylistInfo]:
|
||||
if not self.ensure_connection():
|
||||
logger.error("Not connected to Plex server")
|
||||
return []
|
||||
|
|
@ -281,7 +225,7 @@ class PlexClient(MediaServerClient):
|
|||
try:
|
||||
for playlist in self.server.playlists():
|
||||
if getattr(playlist, 'playlistType', None) == 'audio':
|
||||
playlist_info = PlexPlaylistInfo.from_plex_playlist(playlist)
|
||||
playlist_info = PlaylistInfo.from_plex_playlist(playlist)
|
||||
playlists.append(playlist_info)
|
||||
|
||||
logger.info(f"Retrieved {len(playlists)} audio playlists")
|
||||
|
|
@ -291,14 +235,14 @@ class PlexClient(MediaServerClient):
|
|||
logger.error(f"Error fetching playlists: {e}")
|
||||
return []
|
||||
|
||||
def get_playlist_by_name(self, name: str) -> Optional[PlexPlaylistInfo]:
|
||||
def get_playlist_by_name(self, name: str) -> Optional[PlaylistInfo]:
|
||||
if not self.ensure_connection():
|
||||
return None
|
||||
|
||||
try:
|
||||
playlist = self.server.playlist(name)
|
||||
if getattr(playlist, 'playlistType', None) == 'audio':
|
||||
return PlexPlaylistInfo.from_plex_playlist(playlist)
|
||||
return PlaylistInfo.from_plex_playlist(playlist)
|
||||
return None
|
||||
|
||||
except NotFound:
|
||||
|
|
@ -314,14 +258,14 @@ class PlexClient(MediaServerClient):
|
|||
return False
|
||||
|
||||
try:
|
||||
# Handle both PlexTrackInfo objects and actual Plex track objects
|
||||
# Handle both TrackInfo objects and actual Plex track objects
|
||||
plex_tracks = []
|
||||
for track in tracks:
|
||||
if hasattr(track, 'ratingKey'):
|
||||
# This is already a Plex track object
|
||||
plex_tracks.append(track)
|
||||
elif hasattr(track, '_original_plex_track'):
|
||||
# This is a PlexTrackInfo object with stored original track reference
|
||||
# This is a TrackInfo object with stored original track reference
|
||||
original_track = track._original_plex_track
|
||||
if original_track is not None:
|
||||
plex_tracks.append(original_track)
|
||||
|
|
@ -329,7 +273,7 @@ class PlexClient(MediaServerClient):
|
|||
else:
|
||||
logger.warning(f"Stored track reference is None for: {track.title} by {track.artist}")
|
||||
elif hasattr(track, 'title'):
|
||||
# Fallback: This is a PlexTrackInfo object, need to find the actual track
|
||||
# Fallback: This is a TrackInfo object, need to find the actual track
|
||||
plex_track = self._find_track(track.title, track.artist, track.album)
|
||||
if plex_track:
|
||||
plex_tracks.append(plex_track)
|
||||
|
|
@ -451,7 +395,7 @@ class PlexClient(MediaServerClient):
|
|||
logger.error(f"Error copying playlist '{source_name}' to '{target_name}': {e}")
|
||||
return False
|
||||
|
||||
def update_playlist(self, playlist_name: str, tracks: List[PlexTrackInfo]) -> bool:
|
||||
def update_playlist(self, playlist_name: str, tracks: List[TrackInfo]) -> bool:
|
||||
if not self.ensure_connection():
|
||||
return False
|
||||
|
||||
|
|
@ -522,7 +466,7 @@ class PlexClient(MediaServerClient):
|
|||
logger.error(f"Error searching for track '{title}' by '{artist}': {e}")
|
||||
return None
|
||||
|
||||
def search_tracks(self, title: str, artist: str, limit: int = 15) -> List[PlexTrackInfo]:
|
||||
def search_tracks(self, title: str, artist: str, limit: int = 15) -> List[TrackInfo]:
|
||||
"""
|
||||
Searches for tracks using an efficient, multi-stage "early exit" strategy.
|
||||
It stops and returns results as soon as candidates are found.
|
||||
|
|
@ -557,7 +501,7 @@ class PlexClient(MediaServerClient):
|
|||
# --- Early Exit: If Stage 1 found results, stop here ---
|
||||
if candidate_tracks:
|
||||
logger.info(f"Found {len(candidate_tracks)} candidates in Stage 1. Exiting early.")
|
||||
tracks = [PlexTrackInfo.from_plex_track(track) for track in candidate_tracks[:limit]]
|
||||
tracks = [TrackInfo.from_plex_track(track) for track in candidate_tracks[:limit]]
|
||||
# Store references to original tracks for playlist creation
|
||||
for i, track_info in enumerate(tracks):
|
||||
if i < len(candidate_tracks):
|
||||
|
|
@ -576,7 +520,7 @@ class PlexClient(MediaServerClient):
|
|||
# --- Early Exit: If Stage 2 found results, stop here ---
|
||||
if candidate_tracks:
|
||||
logger.info(f"Found {len(candidate_tracks)} candidates in Stage 2. Exiting early.")
|
||||
tracks = [PlexTrackInfo.from_plex_track(track) for track in candidate_tracks[:limit]]
|
||||
tracks = [TrackInfo.from_plex_track(track) for track in candidate_tracks[:limit]]
|
||||
# Store references to original tracks for playlist creation
|
||||
for i, track_info in enumerate(tracks):
|
||||
if i < len(candidate_tracks):
|
||||
|
|
@ -590,7 +534,7 @@ class PlexClient(MediaServerClient):
|
|||
# Removed to prevent false positives where tracks with same title
|
||||
# but different artists are incorrectly matched
|
||||
|
||||
tracks = [PlexTrackInfo.from_plex_track(track) for track in candidate_tracks[:limit]]
|
||||
tracks = [TrackInfo.from_plex_track(track) for track in candidate_tracks[:limit]]
|
||||
|
||||
# Store references to original tracks for playlist creation
|
||||
for i, track_info in enumerate(tracks):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ from dataclasses import dataclass
|
|||
from datetime import datetime
|
||||
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.plex_client import PlexClient
|
||||
from core.media_server.types import TrackInfo
|
||||
from core.jellyfin_client import JellyfinClient
|
||||
from core.navidrome_client import NavidromeClient
|
||||
from core.soulseek_client import SoulseekClient
|
||||
|
|
@ -456,7 +457,7 @@ class PlaylistSyncService:
|
|||
self.clear_progress_callback(playlist.name)
|
||||
self._cancelled = False
|
||||
|
||||
async def _find_track_in_media_server(self, spotify_track: SpotifyTrack) -> Tuple[Optional[PlexTrackInfo], float]:
|
||||
async def _find_track_in_media_server(self, spotify_track: SpotifyTrack) -> Tuple[Optional[TrackInfo], float]:
|
||||
"""Find a track using the same improved database matching as Download Missing Tracks modal"""
|
||||
try:
|
||||
# Check active media server connection
|
||||
|
|
|
|||
Loading…
Reference in a new issue