diff --git a/requirements.txt b/requirements.txt index 8f16531..2edd217 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ pyobjc-framework-WebKit>=10.0 ; sys_platform == "darwin" pyobjc-framework-AVFoundation>=10.0 ; sys_platform == "darwin" pythonnet==3.0.5 ; sys_platform == "win32" clr-loader==0.2.7.post0 ; sys_platform == "win32" +curl_cffi>=0.9.0 # --- dev only --- pytest>=8.0.0 diff --git a/scripts/refresh_beatport_genres.py b/scripts/refresh_beatport_genres.py new file mode 100644 index 0000000..69a0971 --- /dev/null +++ b/scripts/refresh_beatport_genres.py @@ -0,0 +1,88 @@ +"""Estrae lo slug + numeric id di tutti i generi Beatport. +Uso: python3 scripts/refresh_beatport_genres.py > /tmp/genres.txt +Poi copia manualmente in core/beatport.py::GENRES. + +Nota: la pagina /genres (task originale) restituisce 404 sull'attuale sito +Beatport. Aggreghiamo quindi la lista dei generi dalla pagina /charts, dove +ogni featured chart include un array `genres` completo di slug + id + name. +""" + +from __future__ import annotations + +import re +import json +import sys +from curl_cffi import requests + + +GENRES_URL = "https://www.beatport.com/genres" +CHARTS_URL = "https://www.beatport.com/charts" + + +def _fetch_next_data(url: str) -> dict: + resp = requests.get(url, impersonate="chrome131", timeout=15) + resp.raise_for_status() + m = re.search( + r'', resp.text, re.DOTALL + ) + if not m: + raise RuntimeError(f"__NEXT_DATA__ non trovato su {url}") + return json.loads(m.group(1)) + + +def _collect_genres(node, out: dict) -> None: + """Raccoglie ricorsivamente ogni dict con chiavi slug/id/name che appare + dentro un array chiamato "genres".""" + if isinstance(node, dict): + for k, v in node.items(): + if ( + k == "genres" + and isinstance(v, list) + and v + and all( + isinstance(x, dict) and "slug" in x and "id" in x and "name" in x + for x in v + ) + ): + for g in v: + out[g["id"]] = (g["slug"], g["name"]) + else: + _collect_genres(v, out) + elif isinstance(node, list): + for item in node: + _collect_genres(item, out) + + +def main() -> int: + # Prova /genres (URL originale). Se 404 o schema inatteso, cade su /charts. + try: + data = _fetch_next_data(GENRES_URL) + queries = data["props"]["pageProps"]["dehydratedState"]["queries"] + for q in queries: + d = q.get("state", {}).get("data", {}) + if ( + isinstance(d, dict) + and "results" in d + and isinstance(d["results"], list) + ): + for g in d["results"]: + if "slug" in g and "id" in g and "name" in g: + print(f' "{g["slug"]}": ({g["id"]}, "{g["name"]}"),') + return 0 + except Exception as e: + print(f"# /genres non disponibile ({e}), fallback su /charts", file=sys.stderr) + + data = _fetch_next_data(CHARTS_URL) + genres: dict = {} + _collect_genres(data, genres) + if not genres: + print("nessun genere trovato su /charts", file=sys.stderr) + return 1 + for gid in sorted(genres): + slug, name = genres[gid] + print(f' "{slug}": ({gid}, "{name}"),') + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/fixtures/beatport_melodic_top100.html b/tests/fixtures/beatport_melodic_top100.html new file mode 100644 index 0000000..95687f8 --- /dev/null +++ b/tests/fixtures/beatport_melodic_top100.html @@ -0,0 +1,357 @@ +