beatport: estrazione __NEXT_DATA__ + eccezioni tipizzate
This commit is contained in:
1 parent
4221a5992b
commit
f1e57905c1
2 files changed
+48
No files matched your search
@@ -7,6 +7,8 @@ Vedi docs/superpowers/specs/2026-07-14-beatport-charts-design.md.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@@ -33,6 +35,35 @@ class BeatportTrack:
|
||||
return f"{self.artists} {self.title}"
|
||||
|
||||
|
||||
class BeatportError(Exception):
|
||||
"""Base per errori Beatport."""
|
||||
|
||||
|
||||
class BeatportUnreachableError(BeatportError):
|
||||
"""Rete o server Beatport non raggiungibile / 5xx."""
|
||||
|
||||
|
||||
class BeatportParseError(BeatportError):
|
||||
"""HTML/JSON ricevuto ma non conforme allo schema atteso."""
|
||||
|
||||
|
||||
_NEXT_DATA_RE = re.compile(
|
||||
r'<script id="__NEXT_DATA__"[^>]*>(.+?)</script>',
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def _extract_next_data(html: str) -> dict:
|
||||
"""Estrae il payload JSON dallo script <__NEXT_DATA__> di Next.js."""
|
||||
m = _NEXT_DATA_RE.search(html)
|
||||
if not m:
|
||||
raise BeatportParseError("__NEXT_DATA__ non trovato nella pagina Beatport")
|
||||
try:
|
||||
return json.loads(m.group(1))
|
||||
except json.JSONDecodeError as e:
|
||||
raise BeatportParseError(f"__NEXT_DATA__ JSON malformato: {e}") from e
|
||||
|
||||
|
||||
# Mappa slug URL Beatport -> (numeric_id, display_name)
|
||||
# Enumerata via scripts/refresh_beatport_genres.py (Task 1).
|
||||
# Copia dell'output di /tmp/genres_output.txt (31 generi).
|
||||
|
||||
@@ -66,3 +66,20 @@ class TestBeatportTrack:
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
t.title = "Z" # frozen=True impedisce mutazione
|
||||
|
||||
|
||||
class TestExtractNextData:
|
||||
def test_extracts_json_from_valid_html(self, fixtures_dir):
|
||||
html = (fixtures_dir / "beatport_melodic_top100.html").read_text()
|
||||
data = beatport._extract_next_data(html)
|
||||
assert isinstance(data, dict)
|
||||
assert "props" in data
|
||||
|
||||
def test_missing_script_raises(self):
|
||||
with pytest.raises(beatport.BeatportParseError, match="__NEXT_DATA__ non trovato"):
|
||||
beatport._extract_next_data("<html><body>nulla</body></html>")
|
||||
|
||||
def test_malformed_json_raises(self):
|
||||
broken = '<script id="__NEXT_DATA__" type="application/json">{not: valid}</script>'
|
||||
with pytest.raises(beatport.BeatportParseError, match="JSON malformato"):
|
||||
beatport._extract_next_data(broken)
|
||||
Reference in new issue
Block a user