diff --git a/core/traxsource.py b/core/traxsource.py new file mode 100644 index 0000000..85d213b --- /dev/null +++ b/core/traxsource.py @@ -0,0 +1,61 @@ +"""Fetch Top 100 Traxsource per genere. + +Approccio: session curl_cffi (bypass CF via cookie) + BeautifulSoup HTML scraping. +Spec: docs/superpowers/specs/2026-08-01-traxsource-charts-design.md. +""" + +from __future__ import annotations + +from dataclasses import dataclass + + +# Generi musicali Traxsource (slug -> (id, display_name)). +# Aggiornato via scripts/refresh_traxsource_genres.py. Esclusi "sounds/samples/loops", +# "acapella", "beats", "efx-dj-tools", "stems" (non generi ma tipi di prodotto). +GENRES: dict = { + "afro-house": (27, "Afro House"), + "afro-latin-brazilian": (23, "Afro / Latin / Brazilian"), + "broken-beat-nu-jazz": (2, "Broken Beat / Nu Jazz"), + "classic-house": (12, "Classic House"), + "deep-house": (13, "Deep House"), + "drum-and-bass": (31, "Drum & Bass"), + "electro-house": (11, "Electro House"), + "electronica": (5, "Electronica"), + "garage": (29, "Garage"), + "house": (4, "House"), + "jackin-house": (15, "Jackin House"), + "leftfield": (14, "Leftfield"), + "lounge-chill-out": (1, "Lounge / Chill Out"), + "melodic-progressive-house": (19, "Melodic / Progressive House"), + "minimal-deep-tech": (16, "Minimal / Deep Tech"), + "nu-disco-indie-dance": (17, "Nu Disco / Indie Dance"), + "pop-dance": (32, "Pop Dance"), + "r-and-b-hip-hop": (6, "R&B / Hip Hop"), + "soul-funk-disco": (3, "Soul / Funk / Disco"), + "soulful-house": (24, "Soulful House"), + "tech-house": (18, "Tech House"), + "techno": (20, "Techno"), + "world": (30, "World"), +} + + +@dataclass(frozen=True) +class TraxsourceTrack: + position: int + title: str + mix: str + artists: str + label: str + traxsource_id: int + slug: str + image_url: str = "" + cover_url_large: str = "" + + +def list_genres() -> list: + result = [ + {"slug": slug, "id": gid, "name": name} + for slug, (gid, name) in GENRES.items() + ] + result.sort(key=lambda g: g["name"].casefold()) + return result diff --git a/tests/test_traxsource.py b/tests/test_traxsource.py new file mode 100644 index 0000000..c1eb3da --- /dev/null +++ b/tests/test_traxsource.py @@ -0,0 +1,47 @@ +"""Test per core.traxsource.""" + +from __future__ import annotations + +import pytest + +from core import traxsource + + +class TestListGenres: + def test_returns_list_of_dicts(self): + result = traxsource.list_genres() + assert isinstance(result, list) + assert len(result) >= 10 + for g in result: + assert set(g.keys()) == {"slug", "id", "name"} + assert isinstance(g["slug"], str) and g["slug"] + assert isinstance(g["id"], int) and g["id"] > 0 + assert isinstance(g["name"], str) and g["name"] + + def test_sorted_alphabetically_by_name(self): + result = traxsource.list_genres() + names = [g["name"] for g in result] + assert names == sorted(names, key=str.casefold) + + def test_tech_house_present(self): + result = traxsource.list_genres() + slugs = [g["slug"] for g in result] + assert "tech-house" in slugs + + +class TestTraxsourceTrack: + def test_frozen(self): + t = traxsource.TraxsourceTrack( + position=1, title="X", mix="Y", artists="A", label="L", + traxsource_id=1, slug="x", + ) + with pytest.raises(Exception): + t.title = "Z" + + def test_defaults(self): + t = traxsource.TraxsourceTrack( + position=1, title="X", mix="", artists="A", label="", + traxsource_id=1, slug="x", + ) + assert t.image_url == "" + assert t.cover_url_large == ""