From 13c8e607127eb05932834521b429099e57776204 Mon Sep 17 00:00:00 2001 From: luciano Date: Tue, 14 Jul 2026 16:22:21 +0200 Subject: [PATCH] =?UTF-8?q?beatport:=20parse=20JSON=20=E2=86=92=20list[Bea?= =?UTF-8?q?tportTrack]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/beatport.py | 56 ++++++++++++++++++++++++++++++++++++++++++ tests/test_beatport.py | 28 +++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/core/beatport.py b/core/beatport.py index 8910548..a4d5ff2 100644 --- a/core/beatport.py +++ b/core/beatport.py @@ -110,3 +110,59 @@ def list_genres() -> list: ] result.sort(key=lambda g: g["name"].casefold()) return result + + +def _find_tracks_results(data: dict) -> list: + """Cerca dentro le queries dehydrated il primo `results` che ha almeno 50 elementi + e la shape di una track (chiave `id` presente).""" + try: + queries = data["props"]["pageProps"]["dehydratedState"]["queries"] + except (KeyError, TypeError) as e: + raise BeatportParseError( + f"schema JSON inatteso, `results` non localizzabile (chiave mancante: {e})" + ) from e + + for q in queries: + state_data = (q or {}).get("state", {}).get("data") + if not isinstance(state_data, dict): + continue + results = state_data.get("results") + if isinstance(results, list) and len(results) >= 50: + if results and isinstance(results[0], dict) and "id" in results[0]: + return results + raise BeatportParseError("nessun `results` di 50+ track trovato in __NEXT_DATA__") + + +def _format_artists(artists_field: object) -> str: + """Beatport ritorna artists come lista di dict {name, ...}. + Formatta come 'A, B & C' (& prima dell'ultimo).""" + if not isinstance(artists_field, list) or not artists_field: + return "" + names = [a.get("name", "") for a in artists_field if isinstance(a, dict)] + names = [n for n in names if n] + if not names: + return "" + if len(names) == 1: + return names[0] + return ", ".join(names[:-1]) + " & " + names[-1] + + +def _parse_tracks(data: dict) -> list: + """Trasforma i track dict di Beatport in BeatportTrack ordinati per posizione.""" + raw = _find_tracks_results(data) + out: list = [] + for i, item in enumerate(raw, 1): + try: + length_ms = int(item.get("length_ms") or 0) + track = BeatportTrack( + position=i, + title=str(item.get("name") or "").strip(), + mix=str(item.get("mix_name") or "").strip(), + artists=_format_artists(item.get("artists")), + duration_sec=length_ms // 1000, + beatport_id=int(item.get("id") or 0), + ) + except (TypeError, ValueError) as e: + raise BeatportParseError(f"track[{i}] shape inattesa: {e}") from e + out.append(track) + return out diff --git a/tests/test_beatport.py b/tests/test_beatport.py index 1e533dd..4f3400c 100644 --- a/tests/test_beatport.py +++ b/tests/test_beatport.py @@ -83,3 +83,31 @@ class TestExtractNextData: broken = '' with pytest.raises(beatport.BeatportParseError, match="JSON malformato"): beatport._extract_next_data(broken) + + +class TestParseTracks: + def test_extracts_100_tracks(self, fixtures_dir): + html = (fixtures_dir / "beatport_melodic_top100.html").read_text() + data = beatport._extract_next_data(html) + tracks = beatport._parse_tracks(data) + assert len(tracks) == 100 + + def test_positions_are_sequential_1_to_100(self, fixtures_dir): + html = (fixtures_dir / "beatport_melodic_top100.html").read_text() + tracks = beatport._parse_tracks(beatport._extract_next_data(html)) + positions = [t.position for t in tracks] + assert positions == list(range(1, 101)) + + def test_track_shape(self, fixtures_dir): + html = (fixtures_dir / "beatport_melodic_top100.html").read_text() + tracks = beatport._parse_tracks(beatport._extract_next_data(html)) + first = tracks[0] + assert isinstance(first, beatport.BeatportTrack) + assert first.title + assert first.artists + assert first.duration_sec > 0 + assert first.beatport_id > 0 + + def test_schema_missing_results_raises(self): + with pytest.raises(beatport.BeatportParseError, match="results"): + beatport._parse_tracks({"props": {"pageProps": {}}})