From 252d76b907e61fb8ace268b2110b9976e0cf1ca1 Mon Sep 17 00:00:00 2001 From: luciano Date: Tue, 14 Jul 2026 17:14:02 +0200 Subject: [PATCH] spotify: search_tracks() free-form con limit configurabile --- core/spotify_client.py | 34 ++++++++++++++++ tests/test_spotify_client.py | 77 ++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/core/spotify_client.py b/core/spotify_client.py index 77570ed..8d61f19 100644 --- a/core/spotify_client.py +++ b/core/spotify_client.py @@ -280,3 +280,37 @@ def search_track(token: str, query: str): "name": t.get("name", ""), "artists": ", ".join(a.get("name", "") for a in t.get("artists", [])), } + + +def search_tracks(token: str, query: str, limit: int = 50) -> list: + """Cerca brani su Spotify e ritorna una lista di dict. + + Args: + token: access token Spotify + query: query libera (titolo, artista, misto) + limit: max risultati (Spotify cap = 50) + + Returns: + list[dict] con {id, url, name, artists, album, duration_sec}. Vuota se nessun match. + """ + resp = requests.get( + "https://api.spotify.com/v1/search", + headers={"Authorization": f"Bearer {token}"}, + params={"q": query, "type": "track", "limit": min(limit, 50)}, + timeout=15, + ) + resp.raise_for_status() + items = resp.json().get("tracks", {}).get("items", []) + return [_track_to_dict(t) for t in items] + + +def _track_to_dict(t: dict) -> dict: + """Mappa il track object Spotify sul nostro schema uniforme.""" + return { + "id": t.get("id", ""), + "url": t.get("external_urls", {}).get("spotify", ""), + "name": t.get("name", ""), + "artists": ", ".join(a.get("name", "") for a in t.get("artists", [])), + "album": t.get("album", {}).get("name", ""), + "duration_sec": int(t.get("duration_ms", 0)) // 1000, + } diff --git a/tests/test_spotify_client.py b/tests/test_spotify_client.py index 0590c3e..f2e71fb 100644 --- a/tests/test_spotify_client.py +++ b/tests/test_spotify_client.py @@ -68,3 +68,80 @@ class TestSearchTrack: assert params["q"] == "Kapuchon Hot Sauce" assert params["type"] == "track" assert params["limit"] == "1" + + +class TestSearchTracks: + @responses.activate + def test_returns_list_of_tracks(self): + responses.add( + responses.GET, + "https://api.spotify.com/v1/search", + json={ + "tracks": { + "items": [ + { + "id": f"id{i}", + "name": f"Track {i}", + "artists": [{"name": "Solomun"}], + "album": {"name": "Album X"}, + "duration_ms": 300000 + i * 1000, + "external_urls": {"spotify": f"https://open.spotify.com/track/id{i}"}, + } + for i in range(50) + ] + } + }, + status=200, + ) + result = spotify_client.search_tracks("t", "solomun", limit=50) + assert isinstance(result, list) + assert len(result) == 50 + first = result[0] + assert first["id"] == "id0" + assert first["name"] == "Track 0" + assert first["artists"] == "Solomun" + assert first["album"] == "Album X" + assert first["duration_sec"] == 300 + assert first["url"] == "https://open.spotify.com/track/id0" + + @responses.activate + def test_multiple_artists_joined_with_comma(self): + responses.add( + responses.GET, + "https://api.spotify.com/v1/search", + json={"tracks": {"items": [{ + "id": "x", "name": "n", + "artists": [{"name": "A"}, {"name": "B"}, {"name": "C"}], + "album": {"name": "Alb"}, + "duration_ms": 60000, + "external_urls": {"spotify": "u"}, + }]}}, + status=200, + ) + result = spotify_client.search_tracks("t", "q", limit=1) + assert result[0]["artists"] == "A, B, C" + + @responses.activate + def test_empty_query_returns_empty_list(self): + responses.add( + responses.GET, + "https://api.spotify.com/v1/search", + json={"tracks": {"items": []}}, + status=200, + ) + result = spotify_client.search_tracks("t", "no-match", limit=50) + assert result == [] + + @responses.activate + def test_query_params_include_limit(self): + responses.add( + responses.GET, + "https://api.spotify.com/v1/search", + json={"tracks": {"items": []}}, + status=200, + ) + spotify_client.search_tracks("t", "q", limit=25) + params = responses.calls[0].request.params + assert params["q"] == "q" + assert params["type"] == "track" + assert params["limit"] == "25"