diff --git a/backend/spotify_resolve.py b/backend/spotify_resolve.py index 70c4a33..19b8bee 100644 --- a/backend/spotify_resolve.py +++ b/backend/spotify_resolve.py @@ -53,62 +53,69 @@ def _parse_images(imgs_obj): # ─── RESOLVER ──────────────────────────────────────────────────────────────── -def resolve(sp_type, sp_id): - from spotapi import PublicPlaylist, PublicAlbum, PublicSong +def _spotapi_classes(): + import spotapi as _sp + available = [x for x in dir(_sp) if not x.startswith('_')] + eprint(f'[spotapi] exports: {available}') + return _sp, available +def resolve(sp_type, sp_id): + sp_mod, available = _spotapi_classes() url = f'https://open.spotify.com/{sp_type}/{sp_id}' if sp_type == 'track': - song = PublicSong(url) - raw = song.get_song_info() - eprint(f'[spotapi] track response keys: {list(raw.keys()) if isinstance(raw, dict) else type(raw)}') - tr = _parse_track(raw) - if not tr: - # Fallback: raw might already be a flat dict - tr = { - 'trackName': raw.get('name', ''), - 'artistName': raw.get('artist', ''), - 'collectionName': raw.get('album', ''), - 'artworkUrl100': raw.get('image', ''), - 'releaseDate': raw.get('release_date', ''), - 'trackNumber': raw.get('track_number'), - 'trackExplicitness': 'notExplicit', - 'durationMs': raw.get('duration_ms'), - } - name = tr.get('trackName', '') - artist = tr.get('artistName', '') - art = tr.get('artworkUrl100', '') - return {'type': 'track', 'name': name, 'artist': artist, 'artworkUrl': art, 'tracks': [tr]} + cls_name = next((n for n in ('PublicSong', 'Song', 'PublicTrack', 'Track') if n in available), None) + if not cls_name: + raise Exception(f'spotapi has no track class (available: {available})') + song = getattr(sp_mod, cls_name)(url) + raw = song.get_song_info() + eprint(f'[spotapi] track raw keys: {list(raw.keys()) if isinstance(raw, dict) else type(raw)}') + tr = _parse_track(raw) or { + 'trackName': raw.get('name', ''), + 'artistName': raw.get('artist', ''), + 'collectionName': raw.get('album', ''), + 'artworkUrl100': raw.get('image', ''), + 'releaseDate': raw.get('release_date', ''), + 'trackNumber': raw.get('track_number'), + 'trackExplicitness': 'notExplicit', + 'durationMs': raw.get('duration_ms'), + } + return {'type': 'track', 'name': tr.get('trackName', ''), + 'artist': tr.get('artistName', ''), 'artworkUrl': tr.get('artworkUrl100', ''), + 'tracks': [tr]} if sp_type == 'album': - alb = PublicAlbum(url) + cls_name = next((n for n in ('PublicAlbum', 'Album') if n in available), None) + if not cls_name: + raise Exception(f'spotapi has no album class (available: {available})') + alb = getattr(sp_mod, cls_name)(url) alb_info = alb.get_album_info() - eprint(f'[spotapi] album response type: {type(alb_info)}, keys: {list(alb_info.keys()) if isinstance(alb_info, dict) else "?"}') - - # alb_info may be a dict with metadata + items, or just items - meta = alb_info if isinstance(alb_info, dict) else {} - name = meta.get('name', '') - artist = meta.get('artist', '') or meta.get('artists', '') - art = meta.get('image', '') or meta.get('cover', '') - + eprint(f'[spotapi] album raw type={type(alb_info).__name__} keys={list(alb_info.keys()) if isinstance(alb_info, dict) else "?"}') + meta = alb_info if isinstance(alb_info, dict) else {} + name = meta.get('name', '') + artist = meta.get('artist', '') or meta.get('artists', '') + art = meta.get('image', '') or meta.get('cover', '') raw_tracks = meta.get('tracks') or meta.get('items') or (alb_info if isinstance(alb_info, list) else []) - tracks = [t for t in (_parse_track(i) for i in raw_tracks) if t] + tracks = [t for t in (_parse_track(i) for i in raw_tracks) if t] eprint(f'[spotapi] album ok — {len(tracks)} tracks') return {'type': 'album', 'name': name, 'artist': artist, 'artworkUrl': art, 'tracks': tracks} # playlist - pl = PublicPlaylist(url) - pl_meta = pl.get_playlist_info(limit=1) # first call often returns metadata too - eprint(f'[spotapi] playlist meta type: {type(pl_meta)}') + cls_name = next((n for n in ('PublicPlaylist', 'Playlist') if n in available), None) + if not cls_name: + raise Exception(f'spotapi has no playlist class (available: {available})') + pl = getattr(sp_mod, cls_name)(url) + pl_meta = pl.get_playlist_info(limit=1) + eprint(f'[spotapi] playlist first-page type={type(pl_meta).__name__}') - tracks, skipped = [], 0 - offset = 0 + tracks, skipped, offset = [], 0, 0 while True: - page = pl.get_playlist_info(limit=100, offset=offset) + page = pl.get_playlist_info(limit=100, offset=offset) items = page if isinstance(page, list) else (page.get('items') or page.get('tracks') or []) if not items: break for item in items: - if (item.get('itemV2') or item.get('track', {}).get('data', {})).get('__typename') == 'LocalTrackResponseWrapper': + typename = (item.get('itemV2') or {}).get('data', {}).get('__typename', '') + if typename == 'LocalTrackResponseWrapper': skipped += 1; continue tr = _parse_track(item) if tr: tracks.append(tr) @@ -116,10 +123,7 @@ def resolve(sp_type, sp_id): offset += len(items) time.sleep(0.3) - # Extract playlist name/art from first page if available - name = '' - owner = '' - art = '' + name = owner = art = '' if isinstance(pl_meta, dict): name = pl_meta.get('name', '') owner = pl_meta.get('owner', '') or pl_meta.get('owner_name', '')