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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@@ -33,6 +35,35 @@ class BeatportTrack:
|
|||||||
return f"{self.artists} {self.title}"
|
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)
|
# Mappa slug URL Beatport -> (numeric_id, display_name)
|
||||||
# Enumerata via scripts/refresh_beatport_genres.py (Task 1).
|
# Enumerata via scripts/refresh_beatport_genres.py (Task 1).
|
||||||
# Copia dell'output di /tmp/genres_output.txt (31 generi).
|
# Copia dell'output di /tmp/genres_output.txt (31 generi).
|
||||||
|
|||||||
@@ -66,3 +66,20 @@ class TestBeatportTrack:
|
|||||||
)
|
)
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
t.title = "Z" # frozen=True impedisce mutazione
|
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