Discover page itunes integration. Spotify and Itunes will have their own pool
This commit is contained in:
parent
1560726bbc
commit
1d14a8b987
7 changed files with 377 additions and 97 deletions
|
|
@ -371,8 +371,13 @@ class iTunesClient:
|
||||||
|
|
||||||
return albums[:limit]
|
return albums[:limit]
|
||||||
|
|
||||||
def get_album(self, album_id: str) -> Optional[Dict[str, Any]]:
|
def get_album(self, album_id: str, include_tracks: bool = True) -> Optional[Dict[str, Any]]:
|
||||||
"""Get album information - normalized to Spotify format"""
|
"""Get album information with tracks - normalized to Spotify format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
album_id: iTunes album/collection ID
|
||||||
|
include_tracks: If True, also fetches and includes tracks (default True for Spotify compatibility)
|
||||||
|
"""
|
||||||
results = self._lookup(id=album_id)
|
results = self._lookup(id=album_id)
|
||||||
|
|
||||||
for album_data in results:
|
for album_data in results:
|
||||||
|
|
@ -400,7 +405,7 @@ class iTunesClient:
|
||||||
else:
|
else:
|
||||||
album_type = 'album'
|
album_type = 'album'
|
||||||
|
|
||||||
return {
|
album_result = {
|
||||||
'id': str(album_data.get('collectionId', '')),
|
'id': str(album_data.get('collectionId', '')),
|
||||||
'name': album_data.get('collectionName', ''),
|
'name': album_data.get('collectionName', ''),
|
||||||
'images': images,
|
'images': images,
|
||||||
|
|
@ -414,6 +419,16 @@ class iTunesClient:
|
||||||
'_raw_data': album_data
|
'_raw_data': album_data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Include tracks to match Spotify's get_album format
|
||||||
|
if include_tracks:
|
||||||
|
tracks_data = self.get_album_tracks(album_id)
|
||||||
|
if tracks_data and 'items' in tracks_data:
|
||||||
|
album_result['tracks'] = tracks_data
|
||||||
|
else:
|
||||||
|
album_result['tracks'] = {'items': [], 'total': 0}
|
||||||
|
|
||||||
|
return album_result
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_album_tracks(self, album_id: str) -> Optional[Dict[str, Any]]:
|
def get_album_tracks(self, album_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class MetadataService:
|
||||||
|
|
||||||
def _log_initialization(self):
|
def _log_initialization(self):
|
||||||
"""Log initialization status"""
|
"""Log initialization status"""
|
||||||
spotify_status = "✅ Authenticated" if self.spotify.is_authenticated() else "❌ Not authenticated"
|
spotify_status = "✅ Authenticated" if self.spotify.is_spotify_authenticated() else "❌ Not authenticated"
|
||||||
itunes_status = "✅ Available" if self.itunes.is_authenticated() else "❌ Not available"
|
itunes_status = "✅ Available" if self.itunes.is_authenticated() else "❌ Not available"
|
||||||
|
|
||||||
logger.info(f"MetadataService initialized - Spotify: {spotify_status}, iTunes: {itunes_status}")
|
logger.info(f"MetadataService initialized - Spotify: {spotify_status}, iTunes: {itunes_status}")
|
||||||
|
|
@ -52,7 +52,7 @@ class MetadataService:
|
||||||
def get_active_provider(self) -> str:
|
def get_active_provider(self) -> str:
|
||||||
"""
|
"""
|
||||||
Get the currently active metadata provider.
|
Get the currently active metadata provider.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
"spotify" or "itunes"
|
"spotify" or "itunes"
|
||||||
"""
|
"""
|
||||||
|
|
@ -61,14 +61,16 @@ class MetadataService:
|
||||||
elif self.preferred_provider == "itunes":
|
elif self.preferred_provider == "itunes":
|
||||||
return "itunes"
|
return "itunes"
|
||||||
else: # auto
|
else: # auto
|
||||||
return "spotify" if self.spotify.is_authenticated() else "itunes"
|
# Use is_spotify_authenticated() to check actual Spotify auth status
|
||||||
|
# (is_authenticated() always returns True due to iTunes fallback)
|
||||||
|
return "spotify" if self.spotify.is_spotify_authenticated() else "itunes"
|
||||||
|
|
||||||
def _get_client(self):
|
def _get_client(self):
|
||||||
"""Get the appropriate client based on provider selection"""
|
"""Get the appropriate client based on provider selection"""
|
||||||
provider = self.get_active_provider()
|
provider = self.get_active_provider()
|
||||||
|
|
||||||
if provider == "spotify":
|
if provider == "spotify":
|
||||||
if not self.spotify.is_authenticated():
|
if not self.spotify.is_spotify_authenticated():
|
||||||
logger.warning("Spotify requested but not authenticated, falling back to iTunes")
|
logger.warning("Spotify requested but not authenticated, falling back to iTunes")
|
||||||
return self.itunes
|
return self.itunes
|
||||||
return self.spotify
|
return self.spotify
|
||||||
|
|
@ -168,38 +170,38 @@ class MetadataService:
|
||||||
|
|
||||||
def get_user_playlists(self) -> List:
|
def get_user_playlists(self) -> List:
|
||||||
"""Get user playlists (Spotify only)"""
|
"""Get user playlists (Spotify only)"""
|
||||||
if self.get_active_provider() == "spotify" and self.spotify.is_authenticated():
|
if self.spotify.is_spotify_authenticated():
|
||||||
return self.spotify.get_user_playlists()
|
return self.spotify.get_user_playlists()
|
||||||
logger.warning("User playlists only available with Spotify authentication")
|
logger.warning("User playlists only available with Spotify authentication")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_saved_tracks(self) -> List:
|
def get_saved_tracks(self) -> List:
|
||||||
"""Get user's saved/liked tracks (Spotify only)"""
|
"""Get user's saved/liked tracks (Spotify only)"""
|
||||||
if self.get_active_provider() == "spotify" and self.spotify.is_authenticated():
|
if self.spotify.is_spotify_authenticated():
|
||||||
return self.spotify.get_saved_tracks()
|
return self.spotify.get_saved_tracks()
|
||||||
logger.warning("Saved tracks only available with Spotify authentication")
|
logger.warning("Saved tracks only available with Spotify authentication")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_saved_tracks_count(self) -> int:
|
def get_saved_tracks_count(self) -> int:
|
||||||
"""Get count of user's saved tracks (Spotify only)"""
|
"""Get count of user's saved tracks (Spotify only)"""
|
||||||
if self.get_active_provider() == "spotify" and self.spotify.is_authenticated():
|
if self.spotify.is_spotify_authenticated():
|
||||||
return self.spotify.get_saved_tracks_count()
|
return self.spotify.get_saved_tracks_count()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# ==================== Utility Methods ====================
|
# ==================== Utility Methods ====================
|
||||||
|
|
||||||
def is_authenticated(self) -> bool:
|
def is_authenticated(self) -> bool:
|
||||||
"""Check if any provider is available"""
|
"""Check if any provider is available"""
|
||||||
return self.spotify.is_authenticated() or self.itunes.is_authenticated()
|
return self.spotify.is_spotify_authenticated() or self.itunes.is_authenticated()
|
||||||
|
|
||||||
def get_provider_info(self) -> Dict[str, Any]:
|
def get_provider_info(self) -> Dict[str, Any]:
|
||||||
"""Get information about available providers"""
|
"""Get information about available providers"""
|
||||||
return {
|
return {
|
||||||
"active_provider": self.get_active_provider(),
|
"active_provider": self.get_active_provider(),
|
||||||
"spotify_authenticated": self.spotify.is_authenticated(),
|
"spotify_authenticated": self.spotify.is_spotify_authenticated(),
|
||||||
"itunes_available": self.itunes.is_authenticated(),
|
"itunes_available": self.itunes.is_authenticated(),
|
||||||
"preferred_provider": self.preferred_provider,
|
"preferred_provider": self.preferred_provider,
|
||||||
"can_access_user_data": self.spotify.is_authenticated(),
|
"can_access_user_data": self.spotify.is_spotify_authenticated(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def reload_config(self):
|
def reload_config(self):
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,11 @@ class PersonalizedPlaylistsService:
|
||||||
|
|
||||||
def _build_track_dict(self, row, source: str) -> Dict:
|
def _build_track_dict(self, row, source: str) -> Dict:
|
||||||
"""Build a standardized track dictionary from a database row."""
|
"""Build a standardized track dictionary from a database row."""
|
||||||
track_data = row['track_data_json']
|
# Convert sqlite3.Row to dict if needed (Row objects don't support .get())
|
||||||
|
if hasattr(row, 'keys'):
|
||||||
|
row = dict(row)
|
||||||
|
|
||||||
|
track_data = row.get('track_data_json')
|
||||||
if isinstance(track_data, str):
|
if isinstance(track_data, str):
|
||||||
try:
|
try:
|
||||||
track_data = json.loads(track_data)
|
track_data = json.loads(track_data)
|
||||||
|
|
@ -123,11 +127,11 @@ class PersonalizedPlaylistsService:
|
||||||
'track_id': row.get('spotify_track_id') or row.get('itunes_track_id'),
|
'track_id': row.get('spotify_track_id') or row.get('itunes_track_id'),
|
||||||
'spotify_track_id': row.get('spotify_track_id'),
|
'spotify_track_id': row.get('spotify_track_id'),
|
||||||
'itunes_track_id': row.get('itunes_track_id'),
|
'itunes_track_id': row.get('itunes_track_id'),
|
||||||
'track_name': row['track_name'],
|
'track_name': row.get('track_name', 'Unknown'),
|
||||||
'artist_name': row['artist_name'],
|
'artist_name': row.get('artist_name', 'Unknown'),
|
||||||
'album_name': row['album_name'],
|
'album_name': row.get('album_name', 'Unknown'),
|
||||||
'album_cover_url': row['album_cover_url'],
|
'album_cover_url': row.get('album_cover_url'),
|
||||||
'duration_ms': row['duration_ms'],
|
'duration_ms': row.get('duration_ms', 0),
|
||||||
'popularity': row.get('popularity', 0),
|
'popularity': row.get('popularity', 0),
|
||||||
'track_data_json': track_data,
|
'track_data_json': track_data,
|
||||||
'source': source
|
'source': source
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,19 @@ class SpotifyClient:
|
||||||
self._itunes_client = None # Lazy-loaded iTunes fallback
|
self._itunes_client = None # Lazy-loaded iTunes fallback
|
||||||
self._setup_client()
|
self._setup_client()
|
||||||
|
|
||||||
|
def _is_spotify_id(self, id_str: str) -> bool:
|
||||||
|
"""Check if an ID is a Spotify ID (alphanumeric) vs iTunes ID (numeric only)"""
|
||||||
|
if not id_str:
|
||||||
|
return False
|
||||||
|
# Spotify IDs contain letters and numbers, iTunes IDs are purely numeric
|
||||||
|
return not id_str.isdigit()
|
||||||
|
|
||||||
|
def _is_itunes_id(self, id_str: str) -> bool:
|
||||||
|
"""Check if an ID is an iTunes ID (numeric only)"""
|
||||||
|
if not id_str:
|
||||||
|
return False
|
||||||
|
return id_str.isdigit()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _itunes(self):
|
def _itunes(self):
|
||||||
"""Lazy-load iTunes client for fallback when Spotify not authenticated"""
|
"""Lazy-load iTunes client for fallback when Spotify not authenticated"""
|
||||||
|
|
@ -534,9 +547,13 @@ class SpotifyClient:
|
||||||
logger.error(f"Error fetching track details via Spotify: {e}")
|
logger.error(f"Error fetching track details via Spotify: {e}")
|
||||||
# Fall through to iTunes fallback
|
# Fall through to iTunes fallback
|
||||||
|
|
||||||
# iTunes fallback
|
# iTunes fallback - only if ID is numeric (iTunes format)
|
||||||
logger.debug(f"Using iTunes fallback for track details: {track_id}")
|
if self._is_itunes_id(track_id):
|
||||||
return self._itunes.get_track_details(track_id)
|
logger.debug(f"Using iTunes fallback for track details: {track_id}")
|
||||||
|
return self._itunes.get_track_details(track_id)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Cannot use iTunes fallback for Spotify track ID: {track_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
@rate_limited
|
@rate_limited
|
||||||
def get_track_features(self, track_id: str) -> Optional[Dict[str, Any]]:
|
def get_track_features(self, track_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
|
@ -563,9 +580,13 @@ class SpotifyClient:
|
||||||
logger.error(f"Error fetching album via Spotify: {e}")
|
logger.error(f"Error fetching album via Spotify: {e}")
|
||||||
# Fall through to iTunes fallback
|
# Fall through to iTunes fallback
|
||||||
|
|
||||||
# iTunes fallback
|
# iTunes fallback - only if ID is numeric (iTunes format)
|
||||||
logger.debug(f"Using iTunes fallback for album: {album_id}")
|
if self._is_itunes_id(album_id):
|
||||||
return self._itunes.get_album(album_id)
|
logger.debug(f"Using iTunes fallback for album: {album_id}")
|
||||||
|
return self._itunes.get_album(album_id)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Cannot use iTunes fallback for Spotify album ID: {album_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
@rate_limited
|
@rate_limited
|
||||||
def get_album_tracks(self, album_id: str) -> Optional[Dict[str, Any]]:
|
def get_album_tracks(self, album_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
|
@ -602,9 +623,13 @@ class SpotifyClient:
|
||||||
logger.error(f"Error fetching album tracks via Spotify: {e}")
|
logger.error(f"Error fetching album tracks via Spotify: {e}")
|
||||||
# Fall through to iTunes fallback
|
# Fall through to iTunes fallback
|
||||||
|
|
||||||
# iTunes fallback
|
# iTunes fallback - only if ID is numeric (iTunes format)
|
||||||
logger.debug(f"Using iTunes fallback for album tracks: {album_id}")
|
if self._is_itunes_id(album_id):
|
||||||
return self._itunes.get_album_tracks(album_id)
|
logger.debug(f"Using iTunes fallback for album tracks: {album_id}")
|
||||||
|
return self._itunes.get_album_tracks(album_id)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Cannot use iTunes fallback for Spotify album ID: {album_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
@rate_limited
|
@rate_limited
|
||||||
def get_artist_albums(self, artist_id: str, album_type: str = 'album,single', limit: int = 50) -> List[Album]:
|
def get_artist_albums(self, artist_id: str, album_type: str = 'album,single', limit: int = 50) -> List[Album]:
|
||||||
|
|
@ -629,9 +654,13 @@ class SpotifyClient:
|
||||||
logger.error(f"Error fetching artist albums via Spotify: {e}")
|
logger.error(f"Error fetching artist albums via Spotify: {e}")
|
||||||
# Fall through to iTunes fallback
|
# Fall through to iTunes fallback
|
||||||
|
|
||||||
# iTunes fallback
|
# iTunes fallback - only if ID is numeric (iTunes format)
|
||||||
logger.debug(f"Using iTunes fallback for artist albums: {artist_id}")
|
if self._is_itunes_id(artist_id):
|
||||||
return self._itunes.get_artist_albums(artist_id, album_type, limit)
|
logger.debug(f"Using iTunes fallback for artist albums: {artist_id}")
|
||||||
|
return self._itunes.get_artist_albums(artist_id, album_type, limit)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Cannot use iTunes fallback for Spotify artist ID: {artist_id}")
|
||||||
|
return []
|
||||||
|
|
||||||
@rate_limited
|
@rate_limited
|
||||||
def get_user_info(self) -> Optional[Dict[str, Any]]:
|
def get_user_info(self) -> Optional[Dict[str, Any]]:
|
||||||
|
|
@ -662,6 +691,10 @@ class SpotifyClient:
|
||||||
logger.error(f"Error fetching artist via Spotify: {e}")
|
logger.error(f"Error fetching artist via Spotify: {e}")
|
||||||
# Fall through to iTunes fallback
|
# Fall through to iTunes fallback
|
||||||
|
|
||||||
# iTunes fallback
|
# iTunes fallback - only if ID is numeric (iTunes format)
|
||||||
logger.debug(f"Using iTunes fallback for artist: {artist_id}")
|
if self._is_itunes_id(artist_id):
|
||||||
return self._itunes.get_artist(artist_id)
|
logger.debug(f"Using iTunes fallback for artist: {artist_id}")
|
||||||
|
return self._itunes.get_artist(artist_id)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Cannot use iTunes fallback for Spotify artist ID: {artist_id}")
|
||||||
|
return None
|
||||||
|
|
@ -280,6 +280,7 @@ class WatchlistScanner:
|
||||||
def _get_active_client_and_artist_id(self, watchlist_artist: WatchlistArtist):
|
def _get_active_client_and_artist_id(self, watchlist_artist: WatchlistArtist):
|
||||||
"""
|
"""
|
||||||
Get the appropriate client and artist ID based on active provider.
|
Get the appropriate client and artist ID based on active provider.
|
||||||
|
If iTunes ID is missing, searches by artist name to find and cache it.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (client, artist_id, provider_name) or (None, None, None) if no valid ID
|
Tuple of (client, artist_id, provider_name) or (None, None, None) if no valid ID
|
||||||
|
|
@ -296,9 +297,80 @@ class WatchlistScanner:
|
||||||
if watchlist_artist.itunes_artist_id:
|
if watchlist_artist.itunes_artist_id:
|
||||||
return (self.metadata_service.itunes, watchlist_artist.itunes_artist_id, 'itunes')
|
return (self.metadata_service.itunes, watchlist_artist.itunes_artist_id, 'itunes')
|
||||||
else:
|
else:
|
||||||
logger.warning(f"No iTunes ID for {watchlist_artist.artist_name}, cannot scan with iTunes")
|
# No iTunes ID stored - search by name and cache it
|
||||||
return (None, None, None)
|
logger.info(f"No iTunes ID for {watchlist_artist.artist_name}, searching by name...")
|
||||||
|
try:
|
||||||
|
itunes_client = self.metadata_service.itunes
|
||||||
|
search_results = itunes_client.search_artists(watchlist_artist.artist_name, limit=1)
|
||||||
|
if search_results and len(search_results) > 0:
|
||||||
|
itunes_id = search_results[0].id
|
||||||
|
logger.info(f"Found iTunes ID {itunes_id} for {watchlist_artist.artist_name}")
|
||||||
|
# Cache the iTunes ID in the database for future use
|
||||||
|
self.database.update_watchlist_artist_itunes_id(
|
||||||
|
watchlist_artist.spotify_artist_id or str(watchlist_artist.id),
|
||||||
|
itunes_id
|
||||||
|
)
|
||||||
|
return (itunes_client, itunes_id, 'itunes')
|
||||||
|
else:
|
||||||
|
logger.warning(f"Could not find {watchlist_artist.artist_name} on iTunes")
|
||||||
|
return (None, None, None)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error searching iTunes for {watchlist_artist.artist_name}: {e}")
|
||||||
|
return (None, None, None)
|
||||||
|
|
||||||
|
def get_active_client_and_artist_id(self, watchlist_artist: WatchlistArtist):
|
||||||
|
"""
|
||||||
|
Public wrapper for _get_active_client_and_artist_id.
|
||||||
|
Gets the appropriate client and artist ID based on active provider.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (client, artist_id, provider_name) or (None, None, None) if no valid ID
|
||||||
|
"""
|
||||||
|
return self._get_active_client_and_artist_id(watchlist_artist)
|
||||||
|
|
||||||
|
def get_artist_image_url(self, watchlist_artist: WatchlistArtist) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get artist image URL using the active provider.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Image URL string or None if not available
|
||||||
|
"""
|
||||||
|
client, artist_id, provider = self._get_active_client_and_artist_id(watchlist_artist)
|
||||||
|
if not client or not artist_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
artist_data = client.get_artist(artist_id)
|
||||||
|
if artist_data:
|
||||||
|
# Handle both Spotify and iTunes response formats
|
||||||
|
if 'images' in artist_data and artist_data['images']:
|
||||||
|
return artist_data['images'][0].get('url')
|
||||||
|
elif 'image_url' in artist_data:
|
||||||
|
return artist_data['image_url']
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Could not fetch artist image for {watchlist_artist.artist_name}: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_artist_discography_for_watchlist(self, watchlist_artist: WatchlistArtist, last_scan_timestamp: Optional[datetime] = None) -> Optional[List]:
|
||||||
|
"""
|
||||||
|
Get artist's discography using the active provider, with proper ID resolution.
|
||||||
|
This is the provider-aware version of get_artist_discography.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
watchlist_artist: WatchlistArtist object (has both spotify and itunes IDs)
|
||||||
|
last_scan_timestamp: Only return releases after this date (for incremental scans)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of albums or None on error
|
||||||
|
"""
|
||||||
|
client, artist_id, provider = self._get_active_client_and_artist_id(watchlist_artist)
|
||||||
|
if not client or not artist_id:
|
||||||
|
logger.warning(f"No valid client/ID for {watchlist_artist.artist_name}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._get_artist_discography_with_client(client, artist_id, last_scan_timestamp)
|
||||||
|
|
||||||
def scan_all_watchlist_artists(self) -> List[ScanResult]:
|
def scan_all_watchlist_artists(self) -> List[ScanResult]:
|
||||||
"""
|
"""
|
||||||
Scan artists in the watchlist for new releases.
|
Scan artists in the watchlist for new releases.
|
||||||
|
|
@ -562,7 +634,9 @@ class WatchlistScanner:
|
||||||
try:
|
try:
|
||||||
# Check if we have fresh similar artists cached (< 30 days old)
|
# Check if we have fresh similar artists cached (< 30 days old)
|
||||||
if self.database.has_fresh_similar_artists(source_artist_id, days_threshold=30):
|
if self.database.has_fresh_similar_artists(source_artist_id, days_threshold=30):
|
||||||
logger.info(f"Similar artists for {watchlist_artist.artist_name} are cached and fresh, skipping fetch")
|
logger.info(f"Similar artists for {watchlist_artist.artist_name} are cached and fresh, skipping MusicMap fetch")
|
||||||
|
# Even if cached, backfill missing iTunes IDs (seamless dual-source support)
|
||||||
|
self._backfill_similar_artists_itunes_ids(source_artist_id)
|
||||||
else:
|
else:
|
||||||
logger.info(f"Fetching similar artists for {watchlist_artist.artist_name}...")
|
logger.info(f"Fetching similar artists for {watchlist_artist.artist_name}...")
|
||||||
self.update_similar_artists(watchlist_artist)
|
self.update_similar_artists(watchlist_artist)
|
||||||
|
|
@ -812,6 +886,10 @@ class WatchlistScanner:
|
||||||
album_date = datetime(int(year), int(month), 1, tzinfo=timezone.utc)
|
album_date = datetime(int(year), int(month), 1, tzinfo=timezone.utc)
|
||||||
elif len(release_date_str) == 10: # Full date (e.g., "2023-10-15")
|
elif len(release_date_str) == 10: # Full date (e.g., "2023-10-15")
|
||||||
album_date = datetime.strptime(release_date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
album_date = datetime.strptime(release_date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
||||||
|
elif 'T' in release_date_str: # ISO 8601 with time (e.g., "2017-12-08T08:00:00Z" from iTunes)
|
||||||
|
# Strip the time portion and parse just the date
|
||||||
|
date_part = release_date_str.split('T')[0]
|
||||||
|
album_date = datetime.strptime(date_part, "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Unknown release date format: {release_date_str}")
|
logger.warning(f"Unknown release date format: {release_date_str}")
|
||||||
return True # Include if we can't parse
|
return True # Include if we can't parse
|
||||||
|
|
@ -1246,6 +1324,54 @@ class WatchlistScanner:
|
||||||
logger.error(f"Error fetching similar artists from MusicMap: {e}")
|
logger.error(f"Error fetching similar artists from MusicMap: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def _backfill_similar_artists_itunes_ids(self, source_artist_id: str) -> int:
|
||||||
|
"""
|
||||||
|
Backfill missing iTunes IDs for cached similar artists.
|
||||||
|
This ensures seamless dual-source support without clearing cached data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_artist_id: The source artist ID to backfill similar artists for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of similar artists updated with iTunes IDs
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get similar artists that are missing iTunes IDs
|
||||||
|
similar_artists = self.database.get_similar_artists_missing_itunes_ids(source_artist_id)
|
||||||
|
|
||||||
|
if not similar_artists:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
logger.info(f"Backfilling iTunes IDs for {len(similar_artists)} similar artists")
|
||||||
|
|
||||||
|
# Get iTunes client
|
||||||
|
from core.itunes_client import iTunesClient
|
||||||
|
itunes_client = iTunesClient()
|
||||||
|
|
||||||
|
updated_count = 0
|
||||||
|
for similar_artist in similar_artists:
|
||||||
|
try:
|
||||||
|
# Search iTunes by artist name
|
||||||
|
itunes_results = itunes_client.search_artists(similar_artist.similar_artist_name, limit=1)
|
||||||
|
if itunes_results and len(itunes_results) > 0:
|
||||||
|
itunes_id = itunes_results[0].id
|
||||||
|
# Update the similar artist with the iTunes ID
|
||||||
|
if self.database.update_similar_artist_itunes_id(similar_artist.id, itunes_id):
|
||||||
|
updated_count += 1
|
||||||
|
logger.debug(f" Backfilled iTunes ID {itunes_id} for {similar_artist.similar_artist_name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f" Could not backfill iTunes ID for {similar_artist.similar_artist_name}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if updated_count > 0:
|
||||||
|
logger.info(f"Backfilled {updated_count} similar artists with iTunes IDs")
|
||||||
|
|
||||||
|
return updated_count
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error backfilling similar artists iTunes IDs: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
def update_similar_artists(self, watchlist_artist: WatchlistArtist, limit: int = 10) -> bool:
|
def update_similar_artists(self, watchlist_artist: WatchlistArtist, limit: int = 10) -> bool:
|
||||||
"""
|
"""
|
||||||
Fetch and store similar artists for a watchlist artist.
|
Fetch and store similar artists for a watchlist artist.
|
||||||
|
|
@ -1351,8 +1477,21 @@ class WatchlistScanner:
|
||||||
sources_to_process = []
|
sources_to_process = []
|
||||||
|
|
||||||
# Always add iTunes first (baseline source)
|
# Always add iTunes first (baseline source)
|
||||||
if similar_artist.similar_artist_itunes_id:
|
itunes_id = similar_artist.similar_artist_itunes_id
|
||||||
sources_to_process.append(('itunes', similar_artist.similar_artist_itunes_id))
|
if not itunes_id:
|
||||||
|
# On-the-fly lookup for missing iTunes ID (seamless provider switching)
|
||||||
|
try:
|
||||||
|
itunes_results = itunes_client.search_artists(similar_artist.similar_artist_name, limit=1)
|
||||||
|
if itunes_results and len(itunes_results) > 0:
|
||||||
|
itunes_id = itunes_results[0].id
|
||||||
|
# Cache it for future use
|
||||||
|
self.database.update_similar_artist_itunes_id(similar_artist.id, itunes_id)
|
||||||
|
logger.debug(f" Resolved iTunes ID {itunes_id} for {similar_artist.similar_artist_name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f" Could not resolve iTunes ID for {similar_artist.similar_artist_name}: {e}")
|
||||||
|
|
||||||
|
if itunes_id:
|
||||||
|
sources_to_process.append(('itunes', itunes_id))
|
||||||
|
|
||||||
# Add Spotify if authenticated and we have an ID
|
# Add Spotify if authenticated and we have an ID
|
||||||
if spotify_available and similar_artist.similar_artist_spotify_id:
|
if spotify_available and similar_artist.similar_artist_spotify_id:
|
||||||
|
|
|
||||||
|
|
@ -591,29 +591,7 @@ class MusicDatabase:
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Create indexes for performance
|
# ============== MIGRATIONS (must run BEFORE index creation on new columns) ==============
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_source ON similar_artists (source_artist_id)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_spotify ON similar_artists (similar_artist_spotify_id)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_itunes ON similar_artists (similar_artist_itunes_id)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_occurrence ON similar_artists (occurrence_count)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_name ON similar_artists (similar_artist_name)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_spotify_track ON discovery_pool (spotify_track_id)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_itunes_track ON discovery_pool (itunes_track_id)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_artist ON discovery_pool (spotify_artist_id)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_itunes_artist ON discovery_pool (itunes_artist_id)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_source ON discovery_pool (source)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_added_date ON discovery_pool (added_date)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_is_new ON discovery_pool (is_new_release)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_recent_releases_watchlist ON recent_releases (watchlist_artist_id)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_recent_releases_date ON recent_releases (release_date)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_recent_releases_source ON recent_releases (source)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_recent_albums_source ON discovery_recent_albums (source)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_recent_albums_date ON discovery_recent_albums (release_date)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_listenbrainz_playlists_type ON listenbrainz_playlists (playlist_type)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_listenbrainz_playlists_mbid ON listenbrainz_playlists (playlist_mbid)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_listenbrainz_tracks_playlist ON listenbrainz_tracks (playlist_id)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_listenbrainz_tracks_position ON listenbrainz_tracks (playlist_id, position)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_recent_albums_artist ON discovery_recent_albums (artist_spotify_id)")
|
|
||||||
|
|
||||||
# Add genres column to discovery_pool if it doesn't exist (migration)
|
# Add genres column to discovery_pool if it doesn't exist (migration)
|
||||||
cursor.execute("PRAGMA table_info(discovery_pool)")
|
cursor.execute("PRAGMA table_info(discovery_pool)")
|
||||||
|
|
@ -658,6 +636,30 @@ class MusicDatabase:
|
||||||
cursor.execute("ALTER TABLE discovery_recent_albums ADD COLUMN source TEXT DEFAULT 'spotify'")
|
cursor.execute("ALTER TABLE discovery_recent_albums ADD COLUMN source TEXT DEFAULT 'spotify'")
|
||||||
logger.info("Added iTunes columns to discovery_recent_albums table for dual-source discovery")
|
logger.info("Added iTunes columns to discovery_recent_albums table for dual-source discovery")
|
||||||
|
|
||||||
|
# ============== INDEXES (after migrations to ensure columns exist) ==============
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_source ON similar_artists (source_artist_id)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_spotify ON similar_artists (similar_artist_spotify_id)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_itunes ON similar_artists (similar_artist_itunes_id)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_occurrence ON similar_artists (occurrence_count)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_name ON similar_artists (similar_artist_name)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_spotify_track ON discovery_pool (spotify_track_id)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_itunes_track ON discovery_pool (itunes_track_id)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_artist ON discovery_pool (spotify_artist_id)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_itunes_artist ON discovery_pool (itunes_artist_id)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_source ON discovery_pool (source)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_added_date ON discovery_pool (added_date)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_is_new ON discovery_pool (is_new_release)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_recent_releases_watchlist ON recent_releases (watchlist_artist_id)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_recent_releases_date ON recent_releases (release_date)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_recent_releases_source ON recent_releases (source)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_recent_albums_source ON discovery_recent_albums (source)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_recent_albums_date ON discovery_recent_albums (release_date)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_listenbrainz_playlists_type ON listenbrainz_playlists (playlist_type)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_listenbrainz_playlists_mbid ON listenbrainz_playlists (playlist_mbid)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_listenbrainz_tracks_playlist ON listenbrainz_tracks (playlist_id)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_listenbrainz_tracks_position ON listenbrainz_tracks (playlist_id, position)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_recent_albums_artist ON discovery_recent_albums (artist_spotify_id)")
|
||||||
|
|
||||||
logger.info("Discovery tables created successfully")
|
logger.info("Discovery tables created successfully")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -2996,6 +2998,27 @@ class MusicDatabase:
|
||||||
logger.error(f"Error updating watchlist iTunes ID: {e}")
|
logger.error(f"Error updating watchlist iTunes ID: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def update_watchlist_artist_itunes_id(self, spotify_artist_id: str, itunes_id: str) -> bool:
|
||||||
|
"""Update the iTunes artist ID for a watchlist artist by Spotify ID (for cross-provider caching)"""
|
||||||
|
try:
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE watchlist_artists
|
||||||
|
SET itunes_artist_id = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE spotify_artist_id = ?
|
||||||
|
""", (itunes_id, spotify_artist_id))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
if cursor.rowcount > 0:
|
||||||
|
logger.info(f"Cached iTunes ID {itunes_id} for Spotify artist {spotify_artist_id}")
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error caching watchlist iTunes ID: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
# === Discovery Feature Methods ===
|
# === Discovery Feature Methods ===
|
||||||
|
|
||||||
def add_or_update_similar_artist(self, source_artist_id: str, similar_artist_name: str,
|
def add_or_update_similar_artist(self, source_artist_id: str, similar_artist_name: str,
|
||||||
|
|
@ -3056,10 +3079,66 @@ class MusicDatabase:
|
||||||
logger.error(f"Error getting similar artists: {e}")
|
logger.error(f"Error getting similar artists: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def has_fresh_similar_artists(self, source_artist_id: str, days_threshold: int = 30) -> bool:
|
def get_similar_artists_missing_itunes_ids(self, source_artist_id: str) -> List[SimilarArtist]:
|
||||||
|
"""Get similar artists for a source that are missing iTunes IDs (for backfill)"""
|
||||||
|
try:
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM similar_artists
|
||||||
|
WHERE source_artist_id = ?
|
||||||
|
AND (similar_artist_itunes_id IS NULL OR similar_artist_itunes_id = '')
|
||||||
|
ORDER BY occurrence_count DESC
|
||||||
|
LIMIT 50
|
||||||
|
""", (source_artist_id,))
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
return [SimilarArtist(
|
||||||
|
id=row['id'],
|
||||||
|
source_artist_id=row['source_artist_id'],
|
||||||
|
similar_artist_spotify_id=row['similar_artist_spotify_id'],
|
||||||
|
similar_artist_itunes_id=None,
|
||||||
|
similar_artist_name=row['similar_artist_name'],
|
||||||
|
similarity_rank=row['similarity_rank'],
|
||||||
|
occurrence_count=row['occurrence_count'],
|
||||||
|
last_updated=datetime.fromisoformat(row['last_updated'])
|
||||||
|
) for row in rows]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting similar artists missing iTunes IDs: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def update_similar_artist_itunes_id(self, similar_artist_id: int, itunes_id: str) -> bool:
|
||||||
|
"""Update a similar artist's iTunes ID (for backfill)"""
|
||||||
|
try:
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE similar_artists
|
||||||
|
SET similar_artist_itunes_id = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""", (itunes_id, similar_artist_id))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating similar artist iTunes ID: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_fresh_similar_artists(self, source_artist_id: str, days_threshold: int = 30, require_itunes: bool = True) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if we have cached similar artists that are still fresh (< days_threshold old).
|
Check if we have cached similar artists that are still fresh (< days_threshold old).
|
||||||
Returns True if we have recent data, False if data is stale or missing.
|
Also checks that similar artists have the required provider IDs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_artist_id: The source artist ID to check
|
||||||
|
days_threshold: Maximum age in days to consider fresh
|
||||||
|
require_itunes: If True, also requires iTunes IDs to be present (for seamless provider switching)
|
||||||
|
|
||||||
|
Returns True if we have recent data with required IDs, False if data is stale, missing, or incomplete.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
|
|
@ -3081,7 +3160,27 @@ class MusicDatabase:
|
||||||
last_updated = datetime.fromisoformat(row['last_updated'])
|
last_updated = datetime.fromisoformat(row['last_updated'])
|
||||||
days_since_update = (datetime.now() - last_updated).total_seconds() / 86400 # seconds to days
|
days_since_update = (datetime.now() - last_updated).total_seconds() / 86400 # seconds to days
|
||||||
|
|
||||||
return days_since_update < days_threshold
|
if days_since_update >= days_threshold:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if we have iTunes IDs (for seamless provider switching)
|
||||||
|
if require_itunes:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) as total,
|
||||||
|
SUM(CASE WHEN similar_artist_itunes_id IS NOT NULL AND similar_artist_itunes_id != '' THEN 1 ELSE 0 END) as has_itunes
|
||||||
|
FROM similar_artists
|
||||||
|
WHERE source_artist_id = ?
|
||||||
|
""", (source_artist_id,))
|
||||||
|
id_row = cursor.fetchone()
|
||||||
|
|
||||||
|
if id_row and id_row['total'] > 0:
|
||||||
|
# If less than 50% have iTunes IDs, consider stale and refetch
|
||||||
|
itunes_ratio = id_row['has_itunes'] / id_row['total']
|
||||||
|
if itunes_ratio < 0.5:
|
||||||
|
logger.debug(f"Similar artists for {source_artist_id} missing iTunes IDs ({id_row['has_itunes']}/{id_row['total']}), will refetch")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error checking similar artists freshness: {e}")
|
logger.error(f"Error checking similar artists freshness: {e}")
|
||||||
|
|
|
||||||
|
|
@ -17269,14 +17269,8 @@ def start_watchlist_scan():
|
||||||
|
|
||||||
for i, artist in enumerate(watchlist_artists):
|
for i, artist in enumerate(watchlist_artists):
|
||||||
try:
|
try:
|
||||||
# Fetch artist image
|
# Fetch artist image using provider-aware method
|
||||||
artist_image_url = ''
|
artist_image_url = scanner.get_artist_image_url(artist) or ''
|
||||||
try:
|
|
||||||
artist_data = spotify_client.get_artist(artist.spotify_artist_id)
|
|
||||||
if artist_data and 'images' in artist_data and artist_data['images']:
|
|
||||||
artist_image_url = artist_data['images'][0]['url']
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Update progress
|
# Update progress
|
||||||
watchlist_scan_state.update({
|
watchlist_scan_state.update({
|
||||||
|
|
@ -17290,9 +17284,9 @@ def start_watchlist_scan():
|
||||||
'current_album_image_url': '',
|
'current_album_image_url': '',
|
||||||
'current_track_name': ''
|
'current_track_name': ''
|
||||||
})
|
})
|
||||||
|
|
||||||
# Get artist discography
|
# Get artist discography using provider-aware method
|
||||||
albums = scanner.get_artist_discography(artist.spotify_artist_id, artist.last_scan_timestamp)
|
albums = scanner.get_artist_discography_for_watchlist(artist, artist.last_scan_timestamp)
|
||||||
|
|
||||||
if albums is None:
|
if albums is None:
|
||||||
scan_results.append(type('ScanResult', (), {
|
scan_results.append(type('ScanResult', (), {
|
||||||
|
|
@ -17320,8 +17314,8 @@ def start_watchlist_scan():
|
||||||
# Scan each album
|
# Scan each album
|
||||||
for album_index, album in enumerate(albums):
|
for album_index, album in enumerate(albums):
|
||||||
try:
|
try:
|
||||||
# Get album tracks
|
# Get album tracks using provider-aware method
|
||||||
album_data = scanner.spotify_client.get_album(album.id)
|
album_data = scanner.metadata_service.get_album(album.id)
|
||||||
if not album_data or 'tracks' not in album_data:
|
if not album_data or 'tracks' not in album_data:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -17969,14 +17963,8 @@ def _process_watchlist_scan_automatically():
|
||||||
# Scan each artist with detailed tracking
|
# Scan each artist with detailed tracking
|
||||||
for i, artist in enumerate(watchlist_artists):
|
for i, artist in enumerate(watchlist_artists):
|
||||||
try:
|
try:
|
||||||
# Fetch artist image
|
# Fetch artist image using provider-aware method
|
||||||
artist_image_url = ''
|
artist_image_url = scanner.get_artist_image_url(artist) or ''
|
||||||
try:
|
|
||||||
artist_data = spotify_client.get_artist(artist.spotify_artist_id)
|
|
||||||
if artist_data and 'images' in artist_data and artist_data['images']:
|
|
||||||
artist_image_url = artist_data['images'][0]['url']
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Update progress
|
# Update progress
|
||||||
watchlist_scan_state.update({
|
watchlist_scan_state.update({
|
||||||
|
|
@ -17991,8 +17979,8 @@ def _process_watchlist_scan_automatically():
|
||||||
'current_track_name': ''
|
'current_track_name': ''
|
||||||
})
|
})
|
||||||
|
|
||||||
# Get artist discography
|
# Get artist discography using provider-aware method
|
||||||
albums = scanner.get_artist_discography(artist.spotify_artist_id, artist.last_scan_timestamp)
|
albums = scanner.get_artist_discography_for_watchlist(artist, artist.last_scan_timestamp)
|
||||||
|
|
||||||
if albums is None:
|
if albums is None:
|
||||||
scan_results.append(type('ScanResult', (), {
|
scan_results.append(type('ScanResult', (), {
|
||||||
|
|
@ -18020,8 +18008,8 @@ def _process_watchlist_scan_automatically():
|
||||||
# Scan each album
|
# Scan each album
|
||||||
for album_index, album in enumerate(albums):
|
for album_index, album in enumerate(albums):
|
||||||
try:
|
try:
|
||||||
# Get album tracks
|
# Get album tracks using provider-aware method
|
||||||
album_data = scanner.spotify_client.get_album(album.id)
|
album_data = scanner.metadata_service.get_album(album.id)
|
||||||
if not album_data or 'tracks' not in album_data:
|
if not album_data or 'tracks' not in album_data:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue