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
|
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.
|
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)
|
def eprint(*a): print(*a, file=sys.stderr)
|
||||||
|
|
||||||
@@ -65,34 +65,57 @@ def resolve(sp_type, sp_id):
|
|||||||
|
|
||||||
if sp_type == 'track':
|
if sp_type == 'track':
|
||||||
pub = sp_mod.Public()
|
pub = sp_mod.Public()
|
||||||
# Try URI first, then full URL
|
tr = None
|
||||||
raw = None
|
# Try partner API first (works for some tracks)
|
||||||
for arg in [f'spotify:track:{sp_id}', url]:
|
for arg in [f'spotify:track:{sp_id}', url]:
|
||||||
try:
|
try:
|
||||||
r = pub.song_info(arg)
|
r = pub.song_info(arg)
|
||||||
tu_type = ((r.get('data') or {}).get('trackUnion') or {}).get('__typename', '')
|
tu_type = ((r.get('data') or {}).get('trackUnion') or {}).get('__typename', '')
|
||||||
if tu_type == 'GenericError':
|
if tu_type == 'GenericError':
|
||||||
eprint(f'[spotapi] song_info({arg!r}) → GenericError, trying next')
|
eprint(f'[spotapi] song_info({arg!r}) → GenericError')
|
||||||
continue
|
continue
|
||||||
raw = r
|
d = (r.get('data') or {})
|
||||||
break
|
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:
|
except Exception as e:
|
||||||
eprint(f'[spotapi] song_info({arg!r}) failed: {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')
|
# Fallback: scrape Open Graph tags from the public Spotify track page
|
||||||
eprint(f'[spotapi] track raw keys={list(raw.keys())}')
|
if not tr:
|
||||||
d = (raw.get('data') or {}) if isinstance(raw, dict) else {}
|
eprint(f'[spotapi] falling back to OG scrape for track {sp_id}')
|
||||||
tu = d.get('trackUnion') or d
|
try:
|
||||||
tr = _parse_track({'itemV2': {'data': {'trackUnion': tu}}}) or {
|
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||||
'trackName': raw.get('name', ''),
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||||
'artistName': raw.get('artist', ''),
|
page = resp.read().decode('utf-8', errors='replace')
|
||||||
'collectionName': raw.get('album', ''),
|
og = {m.group(1): _html.unescape(m.group(2))
|
||||||
'artworkUrl100': raw.get('image', ''),
|
for m in re.finditer(r'<meta\s+property="og:([^"]+)"\s+content="([^"]*)"', page)}
|
||||||
'releaseDate': raw.get('release_date', ''),
|
# og:description format: "Song · Artist · Album · Year"
|
||||||
'trackNumber': raw.get('track_number'),
|
desc_parts = [p.strip() for p in og.get('description', '').split('·')]
|
||||||
'trackExplicitness': 'notExplicit',
|
track_name = og.get('title', '')
|
||||||
'durationMs': raw.get('duration_ms'),
|
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', ''),
|
return {'type': 'track', 'name': tr.get('trackName', ''),
|
||||||
'artist': tr.get('artistName', ''), 'artworkUrl': tr.get('artworkUrl100', ''),
|
'artist': tr.get('artistName', ''), 'artworkUrl': tr.get('artworkUrl100', ''),
|
||||||
'tracks': [tr]}
|
'tracks': [tr]}
|
||||||
|
|||||||
Reference in new issue
Block a user