fix: scrape Spotify OG tags as fallback for single track resolution
song_info returns GenericError for unauthenticated track lookups. Fall back to scraping og:title/og:description/og:image from the public Spotify track page — description format is "Song · Artist · Album · Year". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
4f52e7b883
commit
c8f82ceba5
1 file changed
+44
-21
+44
-21
@@ -4,7 +4,7 @@ Resolves Spotify URLs via the Spotify partner GraphQL API (api-partner.spotify.c
|
||||
This is the same internal API the Spotify web player uses — separate rate-limit bucket
|
||||
from the public api.spotify.com/v1/ which gets throttled on server IPs.
|
||||
"""
|
||||
import sys, json, re, time
|
||||
import sys, json, re, time, html as _html, urllib.request
|
||||
|
||||
def eprint(*a): print(*a, file=sys.stderr)
|
||||
|
||||
@@ -65,34 +65,57 @@ def resolve(sp_type, sp_id):
|
||||
|
||||
if sp_type == 'track':
|
||||
pub = sp_mod.Public()
|
||||
# Try URI first, then full URL
|
||||
raw = None
|
||||
tr = None
|
||||
# Try partner API first (works for some tracks)
|
||||
for arg in [f'spotify:track:{sp_id}', url]:
|
||||
try:
|
||||
r = pub.song_info(arg)
|
||||
tu_type = ((r.get('data') or {}).get('trackUnion') or {}).get('__typename', '')
|
||||
if tu_type == 'GenericError':
|
||||
eprint(f'[spotapi] song_info({arg!r}) → GenericError, trying next')
|
||||
eprint(f'[spotapi] song_info({arg!r}) → GenericError')
|
||||
continue
|
||||
raw = r
|
||||
break
|
||||
d = (r.get('data') or {})
|
||||
tu = d.get('trackUnion') or d
|
||||
tr = _parse_track({'itemV2': {'data': {'trackUnion': tu}}})
|
||||
if tr and tr.get('trackName'):
|
||||
eprint(f'[spotapi] track resolved via song_info')
|
||||
break
|
||||
tr = None
|
||||
except Exception as e:
|
||||
eprint(f'[spotapi] song_info({arg!r}) failed: {e}')
|
||||
if raw is None:
|
||||
raise Exception(f'song_info returned GenericError for all inputs — track may require auth or be region-locked')
|
||||
eprint(f'[spotapi] track raw keys={list(raw.keys())}')
|
||||
d = (raw.get('data') or {}) if isinstance(raw, dict) else {}
|
||||
tu = d.get('trackUnion') or d
|
||||
tr = _parse_track({'itemV2': {'data': {'trackUnion': tu}}}) 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'),
|
||||
}
|
||||
|
||||
# Fallback: scrape Open Graph tags from the public Spotify track page
|
||||
if not tr:
|
||||
eprint(f'[spotapi] falling back to OG 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'<meta\s+property="og:([^"]+)"\s+content="([^"]*)"', page)}
|
||||
# og:description format: "Song · Artist · Album · Year"
|
||||
desc_parts = [p.strip() for p in og.get('description', '').split('·')]
|
||||
track_name = og.get('title', '')
|
||||
artist_name = desc_parts[1] if len(desc_parts) > 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}')
|
||||
if track_name:
|
||||
tr = {
|
||||
'trackName': track_name,
|
||||
'artistName': artist_name,
|
||||
'collectionName': album_name,
|
||||
'artworkUrl100': art_url,
|
||||
'releaseDate': '',
|
||||
'trackNumber': None,
|
||||
'trackExplicitness': 'notExplicit',
|
||||
'durationMs': None,
|
||||
}
|
||||
except Exception as e:
|
||||
eprint(f'[spotapi] OG scrape failed: {e}')
|
||||
|
||||
if not tr:
|
||||
raise Exception(f'Track resolution failed: song_info returned GenericError and OG scrape found no title')
|
||||
return {'type': 'track', 'name': tr.get('trackName', ''),
|
||||
'artist': tr.get('artistName', ''), 'artworkUrl': tr.get('artworkUrl100', ''),
|
||||
'tracks': [tr]}
|
||||
|
||||
Reference in new issue
Block a user