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'', 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')