Fix iTunes discovery playlists: synthetic popularity, EP inclusion, and seasonal iTunes support
This commit is contained in:
parent
7da7f3b112
commit
c9549e8a9a
2 changed files with 157 additions and 81 deletions
|
|
@ -298,7 +298,7 @@ class SeasonalDiscoveryService:
|
|||
|
||||
# Source 3: General search for seasonal content
|
||||
logger.info(f"Searching {source} for {season_key} albums...")
|
||||
search_albums = self._search_spotify_seasonal_albums(season_key, limit=50)
|
||||
search_albums = self._search_seasonal_albums(season_key, limit=50)
|
||||
for album in search_albums:
|
||||
if self._add_seasonal_album(season_key, album, source):
|
||||
albums_found += 1
|
||||
|
|
@ -381,11 +381,8 @@ class SeasonalDiscoveryService:
|
|||
return []
|
||||
|
||||
def _search_watchlist_seasonal_albums(self, season_key: str) -> List[Dict]:
|
||||
"""Search for seasonal albums from watchlist artists"""
|
||||
"""Search for seasonal albums from watchlist artists (Spotify + iTunes)"""
|
||||
try:
|
||||
if not self.spotify_client or not self.spotify_client.is_authenticated():
|
||||
return []
|
||||
|
||||
config = SEASONAL_CONFIG[season_key]
|
||||
keywords = config['keywords']
|
||||
|
||||
|
|
@ -394,18 +391,29 @@ class SeasonalDiscoveryService:
|
|||
return []
|
||||
|
||||
seasonal_albums = []
|
||||
source = self._get_source()
|
||||
use_spotify = self.spotify_client and self.spotify_client.is_authenticated()
|
||||
|
||||
# IMPROVED: Sample 20 random watchlist artists (up from 10) for more variety
|
||||
sampled_artists = random.sample(watchlist_artists, min(20, len(watchlist_artists)))
|
||||
|
||||
for artist in sampled_artists:
|
||||
try:
|
||||
# Get artist's albums (including full albums, singles, and EPs)
|
||||
albums = self.spotify_client.get_artist_albums(
|
||||
artist.spotify_artist_id,
|
||||
album_type='album,single,ep',
|
||||
limit=50
|
||||
)
|
||||
albums = []
|
||||
if use_spotify and artist.spotify_artist_id:
|
||||
albums = self.spotify_client.get_artist_albums(
|
||||
artist.spotify_artist_id,
|
||||
album_type='album,single,ep',
|
||||
limit=50
|
||||
) or []
|
||||
elif hasattr(artist, 'itunes_artist_id') and artist.itunes_artist_id:
|
||||
from core.itunes_client import iTunesClient
|
||||
itunes_client = iTunesClient()
|
||||
albums = itunes_client.get_artist_albums(
|
||||
artist.itunes_artist_id,
|
||||
album_type='album,single,ep',
|
||||
limit=50
|
||||
) or []
|
||||
|
||||
# Filter albums by seasonal keywords in title
|
||||
for album in albums:
|
||||
|
|
@ -418,7 +426,8 @@ class SeasonalDiscoveryService:
|
|||
'artist_name': artist.artist_name,
|
||||
'album_cover_url': album.image_url if hasattr(album, 'image_url') else None,
|
||||
'release_date': album.release_date if hasattr(album, 'release_date') else None,
|
||||
'popularity': getattr(album, 'popularity', 50)
|
||||
'popularity': getattr(album, 'popularity', 50),
|
||||
'_source': source
|
||||
})
|
||||
|
||||
import time
|
||||
|
|
@ -434,24 +443,23 @@ class SeasonalDiscoveryService:
|
|||
logger.error(f"Error searching watchlist seasonal albums: {e}")
|
||||
return []
|
||||
|
||||
def _search_spotify_seasonal_albums(self, season_key: str, limit: int = 50) -> List[Dict]:
|
||||
def _search_seasonal_albums(self, season_key: str, limit: int = 50) -> List[Dict]:
|
||||
"""
|
||||
Search Spotify for seasonal albums using keyword search.
|
||||
Search for seasonal albums using keyword search (Spotify or iTunes).
|
||||
|
||||
IMPROVED: Searches more broadly for full albums to get larger track pools.
|
||||
"""
|
||||
try:
|
||||
if not self.spotify_client or not self.spotify_client.is_authenticated():
|
||||
return []
|
||||
|
||||
config = SEASONAL_CONFIG[season_key]
|
||||
keywords = config['keywords']
|
||||
source = self._get_source()
|
||||
use_spotify = self.spotify_client and self.spotify_client.is_authenticated()
|
||||
|
||||
seasonal_albums = []
|
||||
seen_album_ids = set()
|
||||
|
||||
# IMPROVED: Search with top 5 keywords (up from 3) for more variety
|
||||
search_keywords = keywords[:5]
|
||||
search_keywords = list(keywords[:5])
|
||||
|
||||
# Add specific "album" searches to prioritize full albums over singles
|
||||
season_name = config['name'].lower()
|
||||
|
|
@ -461,41 +469,71 @@ class SeasonalDiscoveryService:
|
|||
elif 'halloween' in season_name:
|
||||
search_keywords.append('halloween album')
|
||||
|
||||
for keyword in search_keywords:
|
||||
try:
|
||||
# IMPROVED: Get 20 albums per keyword (up from 10)
|
||||
search_results = self.spotify_client.search_albums(keyword, limit=20)
|
||||
if use_spotify:
|
||||
for keyword in search_keywords:
|
||||
try:
|
||||
search_results = self.spotify_client.search_albums(keyword, limit=20)
|
||||
|
||||
for album in search_results:
|
||||
if album.id in seen_album_ids:
|
||||
continue
|
||||
for album in search_results:
|
||||
if album.id in seen_album_ids:
|
||||
continue
|
||||
|
||||
seen_album_ids.add(album.id)
|
||||
seen_album_ids.add(album.id)
|
||||
|
||||
seasonal_albums.append({
|
||||
'spotify_album_id': album.id,
|
||||
'album_name': album.name,
|
||||
'artist_name': ', '.join(album.artists) if album.artists else 'Various Artists',
|
||||
'album_cover_url': album.image_url if hasattr(album, 'image_url') else None,
|
||||
'release_date': album.release_date if hasattr(album, 'release_date') else None,
|
||||
'popularity': getattr(album, 'popularity', 50)
|
||||
})
|
||||
seasonal_albums.append({
|
||||
'spotify_album_id': album.id,
|
||||
'album_name': album.name,
|
||||
'artist_name': ', '.join(album.artists) if album.artists else 'Various Artists',
|
||||
'album_cover_url': album.image_url if hasattr(album, 'image_url') else None,
|
||||
'release_date': album.release_date if hasattr(album, 'release_date') else None,
|
||||
'popularity': getattr(album, 'popularity', 50)
|
||||
})
|
||||
|
||||
import time
|
||||
time.sleep(0.3) # Rate limiting
|
||||
import time
|
||||
time.sleep(0.3) # Rate limiting
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error searching Spotify for '{keyword}': {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.debug(f"Error searching Spotify for '{keyword}': {e}")
|
||||
continue
|
||||
else:
|
||||
# iTunes fallback
|
||||
from core.itunes_client import iTunesClient
|
||||
itunes_client = iTunesClient()
|
||||
|
||||
logger.info(f"Found {len(seasonal_albums)} seasonal albums from Spotify search")
|
||||
for keyword in search_keywords:
|
||||
try:
|
||||
search_results = itunes_client.search_albums(keyword, limit=20)
|
||||
|
||||
for album in search_results:
|
||||
if album.id in seen_album_ids:
|
||||
continue
|
||||
|
||||
seen_album_ids.add(album.id)
|
||||
|
||||
seasonal_albums.append({
|
||||
'spotify_album_id': album.id, # Column name is spotify_album_id but stores iTunes ID too
|
||||
'album_name': album.name,
|
||||
'artist_name': ', '.join(album.artists) if album.artists else 'Various Artists',
|
||||
'album_cover_url': album.image_url if hasattr(album, 'image_url') else None,
|
||||
'release_date': album.release_date if hasattr(album, 'release_date') else None,
|
||||
'popularity': 50 # iTunes has no popularity — default mid-range
|
||||
})
|
||||
|
||||
import time
|
||||
time.sleep(0.3) # Rate limiting
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error searching iTunes for '{keyword}': {e}")
|
||||
continue
|
||||
|
||||
logger.info(f"Found {len(seasonal_albums)} seasonal albums from {source} search")
|
||||
|
||||
# Return up to limit, prioritizing albums with higher popularity
|
||||
seasonal_albums.sort(key=lambda a: a.get('popularity', 0), reverse=True)
|
||||
return seasonal_albums[:limit]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching Spotify seasonal albums: {e}")
|
||||
logger.error(f"Error searching seasonal albums: {e}")
|
||||
return []
|
||||
|
||||
def _add_seasonal_album(self, season_key: str, album_data: Dict, source: str = 'spotify') -> bool:
|
||||
|
|
@ -675,46 +713,56 @@ class SeasonalDiscoveryService:
|
|||
# Get tracks from seasonal albums (filtered by source)
|
||||
seasonal_albums = self.get_seasonal_albums(season_key, limit=50, source=source)
|
||||
|
||||
use_spotify = self.spotify_client and self.spotify_client.is_authenticated()
|
||||
|
||||
for album in seasonal_albums:
|
||||
try:
|
||||
if not self.spotify_client or not self.spotify_client.is_authenticated():
|
||||
break
|
||||
album_data = None
|
||||
album_id = album['spotify_album_id']
|
||||
|
||||
# Get album tracks
|
||||
album_data = self.spotify_client.get_album(album['spotify_album_id'])
|
||||
if album_data and 'tracks' in album_data:
|
||||
for track in album_data['tracks'].get('items', []):
|
||||
# Use track's actual artist, not album artist
|
||||
track_artist = track['artists'][0]['name'] if track.get('artists') else album['artist_name']
|
||||
if use_spotify:
|
||||
album_data = self.spotify_client.get_album(album_id)
|
||||
else:
|
||||
# iTunes fallback
|
||||
from core.itunes_client import iTunesClient
|
||||
itunes_client = iTunesClient()
|
||||
album_data = itunes_client.get_album(album_id)
|
||||
|
||||
# Enhance track object with full album data (including total_tracks)
|
||||
enhanced_track = {
|
||||
**track,
|
||||
'album': {
|
||||
'id': album_data['id'],
|
||||
'name': album_data.get('name', 'Unknown Album'),
|
||||
'images': album_data.get('images', []),
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'album_type': album_data.get('album_type', 'album'),
|
||||
'total_tracks': album_data.get('total_tracks', 0)
|
||||
}
|
||||
if not album_data or 'tracks' not in album_data:
|
||||
continue
|
||||
|
||||
for track in album_data['tracks'].get('items', []):
|
||||
# Use track's actual artist, not album artist
|
||||
track_artist = track['artists'][0]['name'] if track.get('artists') else album['artist_name']
|
||||
|
||||
# Enhance track object with full album data (including total_tracks)
|
||||
enhanced_track = {
|
||||
**track,
|
||||
'album': {
|
||||
'id': album_data['id'],
|
||||
'name': album_data.get('name', 'Unknown Album'),
|
||||
'images': album_data.get('images', []),
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'album_type': album_data.get('album_type', 'album'),
|
||||
'total_tracks': album_data.get('total_tracks', 0)
|
||||
}
|
||||
}
|
||||
|
||||
track_data = {
|
||||
'spotify_track_id': track['id'],
|
||||
'track_name': track['name'],
|
||||
'artist_name': track_artist,
|
||||
'album_name': album['album_name'],
|
||||
'popularity': album.get('popularity', 50),
|
||||
'album_cover_url': album.get('album_cover_url'),
|
||||
'duration_ms': track.get('duration_ms', 0),
|
||||
'track_data_json': enhanced_track # Add full track data with album info
|
||||
}
|
||||
track_data = {
|
||||
'spotify_track_id': track['id'], # Stores iTunes track ID when on iTunes source
|
||||
'track_name': track['name'],
|
||||
'artist_name': track_artist,
|
||||
'album_name': album['album_name'],
|
||||
'popularity': album.get('popularity', 50),
|
||||
'album_cover_url': album.get('album_cover_url'),
|
||||
'duration_ms': track.get('duration_ms', 0),
|
||||
'track_data_json': enhanced_track
|
||||
}
|
||||
|
||||
all_tracks.append(track_data)
|
||||
all_tracks.append(track_data)
|
||||
|
||||
# Also save track to seasonal_tracks table for later retrieval
|
||||
self._add_seasonal_track(season_key, track_data, source)
|
||||
# Also save track to seasonal_tracks table for later retrieval
|
||||
self._add_seasonal_track(season_key, track_data, source)
|
||||
|
||||
import time
|
||||
time.sleep(0.3) # Rate limiting
|
||||
|
|
|
|||
|
|
@ -1630,7 +1630,7 @@ class WatchlistScanner:
|
|||
else: # itunes
|
||||
all_albums = itunes_client.get_artist_albums(
|
||||
artist_id,
|
||||
album_type='album,single',
|
||||
album_type='album,single,ep',
|
||||
limit=50
|
||||
)
|
||||
|
||||
|
|
@ -1695,9 +1695,8 @@ class WatchlistScanner:
|
|||
album_data = itunes_client.get_album(album.id)
|
||||
if not album_data:
|
||||
continue
|
||||
# iTunes get_album doesn't include tracks inline, need separate call
|
||||
tracks_data = itunes_client.get_album_tracks(album.id)
|
||||
tracks = tracks_data.get('items', []) if tracks_data else []
|
||||
# get_album includes tracks by default (include_tracks=True)
|
||||
tracks = album_data.get('tracks', {}).get('items', [])
|
||||
|
||||
logger.debug(f" Album {album_idx}: {album_data.get('name', 'Unknown')} ({len(tracks)} tracks)")
|
||||
|
||||
|
|
@ -1732,13 +1731,39 @@ class WatchlistScanner:
|
|||
}
|
||||
|
||||
# Build track data for discovery pool with source-specific IDs
|
||||
# iTunes has no popularity data — synthesize from recency + occurrence
|
||||
raw_popularity = album_data.get('popularity', 0)
|
||||
if source == 'itunes' and raw_popularity == 0:
|
||||
# Base 45, boost by recency and artist occurrence count
|
||||
synth_pop = 45
|
||||
if is_new:
|
||||
synth_pop += 25 # New releases get a big boost
|
||||
else:
|
||||
try:
|
||||
release_str = album_data.get('release_date', '')
|
||||
if release_str and len(release_str) >= 10:
|
||||
rel_date = datetime.strptime(release_str[:10], "%Y-%m-%d")
|
||||
age_days = (datetime.now() - rel_date).days
|
||||
if age_days <= 90:
|
||||
synth_pop += 15
|
||||
elif age_days <= 365:
|
||||
synth_pop += 5
|
||||
except:
|
||||
pass
|
||||
# Artists that appear similar to multiple watchlist artists are likely more relevant
|
||||
if similar_artist.occurrence_count >= 3:
|
||||
synth_pop += 10
|
||||
elif similar_artist.occurrence_count >= 2:
|
||||
synth_pop += 5
|
||||
raw_popularity = min(synth_pop, 100)
|
||||
|
||||
track_data = {
|
||||
'track_name': track.get('name', 'Unknown Track'),
|
||||
'artist_name': similar_artist.similar_artist_name,
|
||||
'album_name': album_data.get('name', 'Unknown Album'),
|
||||
'album_cover_url': album_data.get('images', [{}])[0].get('url') if album_data.get('images') else None,
|
||||
'duration_ms': track.get('duration_ms', 0),
|
||||
'popularity': album_data.get('popularity', 0),
|
||||
'popularity': raw_popularity,
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'is_new_release': is_new,
|
||||
'track_data_json': enhanced_track,
|
||||
|
|
@ -2204,7 +2229,7 @@ class WatchlistScanner:
|
|||
if itunes_id:
|
||||
try:
|
||||
albums = itunes_api_call_with_retry(
|
||||
itunes_client.get_artist_albums, itunes_id, album_type='album,single', limit=20
|
||||
itunes_client.get_artist_albums, itunes_id, album_type='album,single,ep', limit=20
|
||||
)
|
||||
for album in albums or []:
|
||||
process_album(album, artist.artist_name, artist.spotify_artist_id, itunes_id, 'itunes')
|
||||
|
|
@ -2252,7 +2277,7 @@ class WatchlistScanner:
|
|||
if itunes_id:
|
||||
try:
|
||||
albums = itunes_api_call_with_retry(
|
||||
itunes_client.get_artist_albums, itunes_id, album_type='album,single', limit=20
|
||||
itunes_client.get_artist_albums, itunes_id, album_type='album,single,ep', limit=20
|
||||
)
|
||||
for album in albums or []:
|
||||
process_album(album, artist.similar_artist_name, artist.similar_artist_spotify_id, itunes_id, 'itunes')
|
||||
|
|
@ -2368,7 +2393,10 @@ class WatchlistScanner:
|
|||
|
||||
# Calculate track score
|
||||
recency_score = max(0, 100 - (days_old * 7))
|
||||
popularity_score = track.get('popularity', album_data.get('popularity', 50))
|
||||
popularity_score = track.get('popularity', album_data.get('popularity', 0))
|
||||
# iTunes has no popularity — use recency-based synthetic score
|
||||
if source == 'itunes' and popularity_score == 0:
|
||||
popularity_score = max(40, 70 - days_old)
|
||||
is_single = album.get('album_type', 'album') == 'single'
|
||||
single_bonus = 20 if is_single else 0
|
||||
total_score = (recency_score * 0.5) + (popularity_score * 0.3) + single_bonus
|
||||
|
|
|
|||
Loading…
Reference in a new issue