♻️ close #57

This commit is contained in:
oskvr37
2025-01-27 21:38:47 +01:00
parent 39b3d38db5
commit e5b38fb537
2 changed files with 140 additions and 109 deletions
+3 -3
View File
@@ -38,8 +38,8 @@ class TestApi(unittest.TestCase):
self.assertEqual(artist.name, "Kanye West") self.assertEqual(artist.name, "Kanye West")
def test_artist_albums(self): def test_artist_albums(self):
self.api.getArtistAlbums(25022) self.api.getArtistAlbums(25022, filter="ALBUMS")
self.api.getArtistAlbums(25022, onlyNonAlbum=True) self.api.getArtistAlbums(25022, filter="EPSANDSINGLES")
def test_album(self): def test_album(self):
album = self.api.getAlbum(103805723) album = self.api.getAlbum(103805723)
@@ -71,7 +71,7 @@ class TestApi(unittest.TestCase):
self.assertGreaterEqual(len(favorites.ARTIST), 0) self.assertGreaterEqual(len(favorites.ARTIST), 0)
def test_search(self): def test_search(self):
self.api.search("Kanye West") self.api.getSearch("Kanye West")
if __name__ == "__main__": if __name__ == "__main__":
+137 -106
View File
@@ -1,57 +1,82 @@
import logging
import json import json
import logging
from pathlib import Path from pathlib import Path
from typing import Any, Literal, Type, TypeVar
from pydantic import BaseModel
from requests import Session from requests import Session
from tiddl.exceptions import AuthError, ApiError
from tiddl.models.api import ( from tiddl.models.api import (
Album,
AlbumItems, AlbumItems,
Artist,
ArtistAlbumsItems, ArtistAlbumsItems,
Favorites, Favorites,
Playlist,
PlaylistItems, PlaylistItems,
SessionResponse,
Search, Search,
SessionResponse,
Track,
TrackStream, TrackStream,
Video,
) )
from tiddl.models.constants import TrackQuality from tiddl.models.constants import TrackQuality
from tiddl.models.resource import Track, Album, Playlist, Artist from tiddl.exceptions import ApiError
DEBUG = False DEBUG = False
API_URL = "https://api.tidal.com/v1" T = TypeVar("T", bound=BaseModel)
# Tidal default limits logger = logging.getLogger(__name__)
ARTIST_ALBUMS_LIMIT = 50
ALBUM_ITEMS_LIMIT = 10
PLAYLIST_LIMIT = 50 def ensureLimit(limit: int, max_limit: int) -> int:
if limit > max_limit:
logger.warning(f"Max limit is {max_limit}")
return max_limit
return limit
class Limits:
ARTIST_ALBUMS = 50
ALBUM_ITEMS = 10
ALBUM_ITEMS_MAX = 100
PLAYLIST = 50
class TidalApi: class TidalApi:
URL = "https://api.tidal.com/v1"
LIMITS = Limits
def __init__(self, token: str, user_id: str, country_code: str) -> None: def __init__(self, token: str, user_id: str, country_code: str) -> None:
self.token = token
self.user_id = user_id self.user_id = user_id
self.country_code = country_code self.country_code = country_code
self._session = Session() self.session = Session()
self._session.headers = {"authorization": f"Bearer {token}"} self.session.headers = {
self._logger = logging.getLogger("TidalApi") "Authorization": f"Bearer {token}",
"Accept": "application/json",
}
def _request(self, endpoint: str, params={}): def fetch(
self._logger.debug(f"{endpoint} {params}") self, model: Type[T], endpoint: str, params: dict[str, Any] = {}
req = self._session.request( ) -> T:
method="GET", url=f"{API_URL}/{endpoint}", params=params """Fetch data from the API and parse it into the given Pydantic model."""
)
req = self.session.get(f"{self.URL}/{endpoint}", params=params)
logger.debug((endpoint, params, req.status_code))
data = req.json() data = req.json()
if req.status_code == 401:
raise AuthError(**data)
if req.status_code != 200:
raise ApiError(**data)
if DEBUG: if DEBUG:
debug_data = {"endpoint": endpoint, "params": params, "data": data} debug_data = {
"status_code": req.status_code,
"endpoint": endpoint,
"params": params,
"data": data,
}
path = Path(f"debug_data/{endpoint}.json") path = Path(f"debug_data/{endpoint}.json")
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
@@ -59,98 +84,104 @@ class TidalApi:
with path.open("w", encoding="utf-8") as f: with path.open("w", encoding="utf-8") as f:
json.dump(debug_data, f, indent=2) json.dump(debug_data, f, indent=2)
return data if req.status_code != 200:
raise ApiError(**data)
def getSession(self): return model.model_validate(data)
return SessionResponse(
**self._request( def getAlbum(self, album_id: str | int):
"sessions", return self.fetch(
) Album, f"albums/{album_id}", {"countryCode": self.country_code}
) )
def getTrackStream(self, id: str | int, quality: TrackQuality): def getAlbumItems(
return TrackStream( self, album_id: str | int, limit=LIMITS.ALBUM_ITEMS, offset=0
**self._request( ):
f"tracks/{id}/playbackinfo", return self.fetch(
{ AlbumItems,
"audioquality": quality, f"albums/{album_id}/items",
"playbackmode": "STREAM", {
"assetpresentation": "FULL", "countryCode": self.country_code,
}, "limit": ensureLimit(limit, self.LIMITS.ALBUM_ITEMS_MAX),
) "offset": offset,
},
) )
def getTrack(self, id: str | int): def getArtist(self, artist_id: str | int):
return Track( return self.fetch(
**self._request(f"tracks/{id}", {"countryCode": self.country_code}) Artist, f"artists/{artist_id}", {"countryCode": self.country_code}
)
def getArtist(self, id: str | int):
return Artist(
**self._request(f"artists/{id}", {"countryCode": self.country_code})
) )
def getArtistAlbums( def getArtistAlbums(
self, id: str | int, limit=ARTIST_ALBUMS_LIMIT, offset=0, onlyNonAlbum=False self,
artist_id: str | int,
limit=LIMITS.ARTIST_ALBUMS,
offset=0,
filter: Literal["ALBUMS", "EPSANDSINGLES"] = "ALBUMS",
): ):
params = {"countryCode": self.country_code, "limit": limit, "offset": offset} return self.fetch(
ArtistAlbumsItems,
if onlyNonAlbum: f"artists/{artist_id}/albums",
params.update({"filter": "EPSANDSINGLES"}) {
"countryCode": self.country_code,
return ArtistAlbumsItems( "limit": limit, # tested limit 10,000
**self._request( "offset": offset,
f"artists/{id}/albums", "filter": filter,
params, },
)
)
def getAlbum(self, id: str | int):
return Album(
**self._request(f"albums/{id}", {"countryCode": self.country_code})
)
def getAlbumItems(self, id: str | int, limit=ALBUM_ITEMS_LIMIT, offset=0):
MAX_LIMIT = 100
if limit > MAX_LIMIT:
logging.warning(f"Too big page, max page size is {MAX_LIMIT}")
limit = MAX_LIMIT
return AlbumItems(
**self._request(
f"albums/{id}/items",
{"countryCode": self.country_code, "limit": limit, "offset": offset},
)
)
def getPlaylist(self, uuid: str):
return Playlist(
**self._request(
f"playlists/{uuid}",
{"countryCode": self.country_code},
)
)
def getPlaylistItems(self, uuid: str, limit=PLAYLIST_LIMIT, offset=0):
return PlaylistItems(
**self._request(
f"playlists/{uuid}/items",
{"countryCode": self.country_code, "limit": limit, "offset": offset},
)
) )
def getFavorites(self): def getFavorites(self):
return Favorites( return self.fetch(
**self._request( Favorites,
f"users/{self.user_id}/favorites/ids", f"users/{self.user_id}/favorites/ids",
{"countryCode": self.country_code}, {"countryCode": self.country_code},
)
) )
def search(self, query: str): def getPlaylist(self, playlist_uuid: str):
return Search( return self.fetch(
**self._request( Playlist,
"search", {"countryCode": self.country_code, "query": query} f"playlists/{playlist_uuid}",
) {"countryCode": self.country_code},
)
def getPlaylistItems(
self, playlist_uuid: str, limit=LIMITS.PLAYLIST, offset=0
):
return self.fetch(
PlaylistItems,
f"playlists/{playlist_uuid}/items",
{
"countryCode": self.country_code,
"limit": limit,
"offset": offset,
},
)
def getSearch(self, query: str):
return self.fetch(
Search, "search", {"countryCode": self.country_code, "query": query}
)
def getSession(self):
return self.fetch(SessionResponse, "sessions")
def getTrack(self, track_id: str | int):
return self.fetch(
Track, f"tracks/{track_id}", {"countryCode": self.country_code}
)
def getTrackStream(self, track_id: str | int, quality: TrackQuality):
return self.fetch(
TrackStream,
f"tracks/{track_id}/playbackinfo",
{
"audioquality": quality,
"playbackmode": "STREAM",
"assetpresentation": "FULL",
},
)
def getVideo(self, video_id: str | int):
return self.fetch(
Video, f"videos/{video_id}", {"countryCode": self.country_code}
) )