diff --git a/backend/spotify_resolve.py b/backend/spotify_resolve.py index 850a359..9b461d8 100644 --- a/backend/spotify_resolve.py +++ b/backend/spotify_resolve.py @@ -84,22 +84,48 @@ def resolve(sp_type, sp_id): except Exception as e: eprint(f'[spotapi] song_info({arg!r}) failed: {e}') - # Fallback: scrape Open Graph tags from the public Spotify track page + # Fallback: scrape the public Spotify track page if not tr: - eprint(f'[spotapi] falling back to OG scrape for track {sp_id}') + eprint(f'[spotapi] falling back to page scrape for track {sp_id}') try: req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) with urllib.request.urlopen(req, timeout=10) as resp: page = resp.read().decode('utf-8', errors='replace') - og = {m.group(1): _html.unescape(m.group(2)) - for m in re.finditer(r' 1 else '' - album_name = desc_parts[2] if len(desc_parts) > 2 else '' - art_url = og.get('image', '') - eprint(f'[spotapi] OG scrape: name={track_name!r} artist={artist_name!r} album={album_name!r}') + + track_name, artist_name, album_name, art_url = '', '', '', '' + + # 1) JSON-LD structured data — most reliable, always has byArtist + ld_m = re.search(r']+type=["\']application/ld\+json["\'][^>]*>(.*?)', page, re.DOTALL | re.IGNORECASE) + if ld_m: + try: + ld = json.loads(ld_m.group(1)) + track_name = ld.get('name', '') + by_artist = ld.get('byArtist', []) + if isinstance(by_artist, list): + artist_name = ', '.join(a.get('name', '') for a in by_artist if a.get('name')) + elif isinstance(by_artist, dict): + artist_name = by_artist.get('name', '') + in_album = ld.get('inAlbum') or {} + album_name = in_album.get('name', '') if isinstance(in_album, dict) else '' + eprint(f'[spotapi] JSON-LD: name={track_name!r} artist={artist_name!r} album={album_name!r}') + except Exception as e: + eprint(f'[spotapi] JSON-LD parse failed: {e}') + + # 2) OG tags — artist is desc_parts[0], then type ("Song"), then year + if not track_name: + og = {} + for m in re.finditer(r']+(?:property|name)=["\']og:([^"\']+)["\']', page): + og[m.group(2)] = _html.unescape(m.group(1)) + desc_parts = [p.strip() for p in og.get('description', '').split('·')] + track_name = og.get('title', '') + # Real format: "Artist · Song · Year · N songs." — artist is index 0 + artist_name = desc_parts[0] if desc_parts else '' + album_name = '' + art_url = og.get('image', '') + eprint(f'[spotapi] OG scrape: name={track_name!r} artist={artist_name!r}') + if track_name: tr = { 'trackName': track_name, @@ -112,7 +138,7 @@ def resolve(sp_type, sp_id): 'durationMs': None, } except Exception as e: - eprint(f'[spotapi] OG scrape failed: {e}') + eprint(f'[spotapi] page scrape failed: {e}') if not tr: raise Exception(f'Track resolution failed: song_info returned GenericError and OG scrape found no title')