fix: OG scrape fallback — parse JSON-LD first, fix artist field index
JSON-LD byArtist gives the correct artist list. As secondary fallback, OG desc format is 'Artist · Song · Year' not 'Song · Artist · Album', so use desc_parts[0] not desc_parts[1] for artist. Also handle both attribute orderings in meta tags. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
d785a4c09a
commit
98ad8ed579
1 file changed
+35
-9
@@ -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'<meta\s+property="og:([^"]+)"\s+content="([^"]*)"', page)}
|
||||
# og:description format: "Song · Artist · Album · Year"
|
||||
|
||||
track_name, artist_name, album_name, art_url = '', '', '', ''
|
||||
|
||||
# 1) JSON-LD structured data — most reliable, always has byArtist
|
||||
ld_m = re.search(r'<script[^>]+type=["\']application/ld\+json["\'][^>]*>(.*?)</script>', 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'<meta\s+(?:property|name)=["\']og:([^"\']+)["\']\s+content=["\']([^"\']*)["\']', page):
|
||||
og[m.group(1)] = _html.unescape(m.group(2))
|
||||
for m in re.finditer(r'<meta\s+content=["\']([^"\']*)["\'][^>]+(?: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', '')
|
||||
artist_name = desc_parts[1] if len(desc_parts) > 1 else ''
|
||||
album_name = desc_parts[2] if len(desc_parts) > 2 else ''
|
||||
# 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} album={album_name!r}')
|
||||
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')
|
||||
|
||||
Reference in new issue
Block a user