diff --git a/tests/test_utils.py b/tests/test_utils.py index 4fbe5f5..a255c21 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ import unittest -from tiddl.models import Track +from tiddl.models.resource import Track from tiddl.utils import TidalResource, formatTrack diff --git a/tiddl/api.py b/tiddl/api.py index ec8228a..7113252 100644 --- a/tiddl/api.py +++ b/tiddl/api.py @@ -4,8 +4,18 @@ import json from pathlib import Path from requests import Session -from tiddl import models -from .exceptions import AuthError, ApiError +from tiddl.exceptions import AuthError, ApiError +from tiddl.models.api import ( + AlbumItems, + ArtistAlbumsItems, + Favorites, + PlaylistItems, + SessionResponse, + TrackStream, +) +from tiddl.models.constants import TrackQuality +from tiddl.models.resource import Track, Album, Playlist +from tiddl.models.search import Search DEBUG = False API_URL = "https://api.tidal.com/v1" @@ -52,14 +62,14 @@ class TidalApi: return data def getSession(self): - return models.SessionResponse( + return SessionResponse( **self._request( "sessions", ) ) - def getTrackStream(self, id: str | int, quality: models.TrackQuality): - return models.TrackStream( + def getTrackStream(self, id: str | int, quality: TrackQuality): + return TrackStream( **self._request( f"tracks/{id}/playbackinfo", { @@ -71,7 +81,7 @@ class TidalApi: ) def getTrack(self, id: str | int): - return models.Track( + return Track( **self._request(f"tracks/{id}", {"countryCode": self.country_code}) ) @@ -83,7 +93,7 @@ class TidalApi: if onlyNonAlbum: params.update({"filter": "EPSANDSINGLES"}) - return models.AristAlbumsItems( + return ArtistAlbumsItems( **self._request( f"artists/{id}/albums", params, @@ -91,7 +101,7 @@ class TidalApi: ) def getAlbum(self, id: str | int): - return models.Album( + return Album( **self._request(f"albums/{id}", {"countryCode": self.country_code}) ) @@ -102,7 +112,7 @@ class TidalApi: logging.warning(f"Too big page, max page size is {MAX_LIMIT}") limit = MAX_LIMIT - return models.AlbumItems( + return AlbumItems( **self._request( f"albums/{id}/items", {"countryCode": self.country_code, "limit": limit, "offset": offset}, @@ -110,7 +120,7 @@ class TidalApi: ) def getPlaylist(self, uuid: str): - return models.Playlist( + return Playlist( **self._request( f"playlists/{uuid}", {"countryCode": self.country_code}, @@ -118,7 +128,7 @@ class TidalApi: ) def getPlaylistItems(self, uuid: str, limit=PLAYLIST_LIMIT, offset=0): - return models.PlaylistItems( + return PlaylistItems( **self._request( f"playlists/{uuid}/items", {"countryCode": self.country_code, "limit": limit, "offset": offset}, @@ -126,7 +136,7 @@ class TidalApi: ) def getFavorites(self): - return models.Favorites( + return Favorites( **self._request( f"users/{self.user_id}/favorites/ids", {"countryCode": self.country_code}, @@ -134,7 +144,7 @@ class TidalApi: ) def search(self, query: str): - return models.Search( + return Search( **self._request( "search", {"countryCode": self.country_code, "query": query} ) diff --git a/tiddl/cli/download/__init__.py b/tiddl/cli/download/__init__.py index 07b6c39..34ba127 100644 --- a/tiddl/cli/download/__init__.py +++ b/tiddl/cli/download/__init__.py @@ -8,10 +8,12 @@ from .url import UrlGroup from ..ctx import Context, passContext from tiddl.download import downloadTrackStream -from tiddl.models import TrackArg, ARG_TO_QUALITY, Track, PlaylistItems, Album from tiddl.utils import formatTrack, trackExists, TidalResource from tiddl.metadata import addMetadata, Cover from tiddl.exceptions import ApiError, AuthError +from tiddl.models.constants import TrackArg, ARG_TO_QUALITY +from tiddl.models.resource import Track, Album +from tiddl.models.api import PlaylistItems @click.command("download") diff --git a/tiddl/config.py b/tiddl/config.py index 5b52700..cde1a84 100644 --- a/tiddl/config.py +++ b/tiddl/config.py @@ -1,7 +1,7 @@ from pydantic import BaseModel from pathlib import Path -from tiddl.models import TrackArg +from tiddl.models.constants import TrackArg CONFIG_PATH = Path.home() / "tiddl.json" diff --git a/tiddl/download.py b/tiddl/download.py index 1e0a966..0018a36 100644 --- a/tiddl/download.py +++ b/tiddl/download.py @@ -5,7 +5,7 @@ from pydantic import BaseModel from base64 import b64decode from xml.etree.ElementTree import fromstring -from tiddl.models import TrackStream +from tiddl.models.api import TrackStream logger = logging.getLogger(__name__) diff --git a/tiddl/metadata.py b/tiddl/metadata.py index 8f8a3b7..8a2673e 100644 --- a/tiddl/metadata.py +++ b/tiddl/metadata.py @@ -7,7 +7,7 @@ from mutagen.flac import FLAC as MutagenFLAC, Picture from mutagen.easymp4 import EasyMP4 as MutagenEasyMP4 from mutagen.mp4 import MP4Cover, MP4 as MutagenMP4 -from tiddl.models import Track +from tiddl.models.resource import Track logger = logging.getLogger(__name__) diff --git a/tiddl/models/__init__.py b/tiddl/models/__init__.py index 7f7eb23..e69de29 100644 --- a/tiddl/models/__init__.py +++ b/tiddl/models/__init__.py @@ -1,16 +0,0 @@ -from typing import TypedDict, Literal - -from .api import * -from .track import * -from .search import * - -TrackArg = Literal["low", "normal", "high", "master"] - -ARG_TO_QUALITY: dict[TrackArg, TrackQuality] = { - "low": "LOW", - "normal": "HIGH", - "high": "LOSSLESS", - "master": "HI_RES_LOSSLESS", -} - -QUALITY_TO_ARG = {v: k for k, v in ARG_TO_QUALITY.items()} diff --git a/tiddl/models/api.py b/tiddl/models/api.py index 1b10cdd..e8eb95d 100644 --- a/tiddl/models/api.py +++ b/tiddl/models/api.py @@ -1,8 +1,17 @@ from pydantic import BaseModel from typing import Optional, List, Literal, Union -from .track import Track -from .resource import Video, Album +from .resource import Video, Album, Track, TrackQuality + +__all__ = [ + "Client", + "SessionResponse", + "ArtistAlbumsItems", + "AlbumItems", + "PlaylistItems", + "Favorites", + "TrackStream", +] class Client(BaseModel): @@ -27,7 +36,7 @@ class Items(BaseModel): totalNumberOfItems: int -class AristAlbumsItems(Items): +class ArtistAlbumsItems(Items): items: List[Album] @@ -80,3 +89,19 @@ class Favorites(BaseModel): VIDEO: List[str] TRACK: List[str] ARTIST: List[str] + + +class TrackStream(BaseModel): + trackId: int + assetPresentation: Literal["FULL"] + audioMode: Literal["STEREO"] + audioQuality: TrackQuality + manifestMimeType: Literal["application/dash+xml", "application/vnd.tidal.bts"] + manifestHash: str + manifest: str + albumReplayGain: float + albumPeakAmplitude: float + trackReplayGain: float + trackPeakAmplitude: float + bitDepth: Optional[int] = None + sampleRate: Optional[int] = None diff --git a/tiddl/models/constants.py b/tiddl/models/constants.py new file mode 100644 index 0000000..94b65b9 --- /dev/null +++ b/tiddl/models/constants.py @@ -0,0 +1,13 @@ +from typing import Literal + +TrackQuality = Literal["LOW", "HIGH", "LOSSLESS", "HI_RES_LOSSLESS"] +TrackArg = Literal["low", "normal", "high", "master"] + +ARG_TO_QUALITY: dict[TrackArg, TrackQuality] = { + "low": "LOW", + "normal": "HIGH", + "high": "LOSSLESS", + "master": "HI_RES_LOSSLESS", +} + +QUALITY_TO_ARG = {v: k for k, v in ARG_TO_QUALITY.items()} diff --git a/tiddl/models/resource.py b/tiddl/models/resource.py index 0715f68..bd44df8 100644 --- a/tiddl/models/resource.py +++ b/tiddl/models/resource.py @@ -1,6 +1,57 @@ from pydantic import BaseModel from datetime import datetime from typing import Optional, List, Literal, Dict +from .constants import TrackQuality + + +__all__ = ["Track", "Video", "Album", "Playlist"] + + +class Track(BaseModel): + + class Artist(BaseModel): + id: int + name: str + type: str + picture: Optional[str] = None + + class Album(BaseModel): + id: int + title: str + cover: Optional[str] = None + vibrantColor: Optional[str] = None + videoCover: Optional[str] = None + + id: int + title: str + duration: int + replayGain: float + peak: float + allowStreaming: bool + streamReady: bool + adSupportedStreamReady: bool + djReady: bool + stemReady: bool + streamStartDate: Optional[datetime] = None + premiumStreamingOnly: bool + trackNumber: int + volumeNumber: int + version: Optional[str] = None + popularity: int + copyright: str + bpm: Optional[int] = None + url: str + isrc: str + editable: bool + explicit: bool + audioQuality: TrackQuality + audioModes: List[str] + mediaMetadata: Dict[str, List[str]] + # for real, artist can be None? + artist: Optional[Artist] = None + artists: List[Artist] + album: Album + mixes: Dict[str, str] class Video(BaseModel): @@ -53,7 +104,7 @@ class Album(BaseModel): picture: Optional[str] = None class MediaMetadata(BaseModel): - tags: List[Literal['LOSSLESS', 'HIRES_LOSSLESS']] + tags: List[Literal["LOSSLESS", "HIRES_LOSSLESS"]] id: int title: str diff --git a/tiddl/models/search.py b/tiddl/models/search.py index 148b6b3..a70b979 100644 --- a/tiddl/models/search.py +++ b/tiddl/models/search.py @@ -1,16 +1,10 @@ from pydantic import BaseModel from typing import Optional, List, Literal, Dict, Union -from .track import Track -from .resource import Playlist, Album, Video +from .resource import Track, Playlist, Album, Video from .api import Items -class SearchAlbum(Album): - # TODO: remove the artist field instead of making it None - artist: None = None - - class Artist(BaseModel): class Role(BaseModel): @@ -40,10 +34,15 @@ class Artist(BaseModel): class Search(BaseModel): + class Artists(Items): items: List[Artist] class Albums(Items): + class SearchAlbum(Album): + # TODO: remove the artist field instead of making it None + artist: None = None + items: List[SearchAlbum] class Playlists(Items): diff --git a/tiddl/models/track.py b/tiddl/models/track.py deleted file mode 100644 index 605277a..0000000 --- a/tiddl/models/track.py +++ /dev/null @@ -1,71 +0,0 @@ -from pydantic import BaseModel -from datetime import datetime -from typing import Optional, List, Dict, Literal - - -TrackQuality = Literal["LOW", "HIGH", "LOSSLESS", "HI_RES_LOSSLESS"] -ManifestMimeType = Literal["application/dash+xml", "application/vnd.tidal.bts"] - - -class TrackStream(BaseModel): - trackId: int - assetPresentation: Literal["FULL"] - audioMode: Literal["STEREO"] - audioQuality: TrackQuality - manifestMimeType: ManifestMimeType - manifestHash: str - manifest: str - albumReplayGain: float - albumPeakAmplitude: float - trackReplayGain: float - trackPeakAmplitude: float - bitDepth: Optional[int] = None - sampleRate: Optional[int] = None - - -class TrackArtist(BaseModel): - id: int - name: str - type: str - picture: Optional[str] = None - - -class TrackAlbum(BaseModel): - id: int - title: str - cover: Optional[str] = None - vibrantColor: Optional[str] = None - videoCover: Optional[str] = None - - -class Track(BaseModel): - id: int - title: str - duration: int - replayGain: float - peak: float - allowStreaming: bool - streamReady: bool - adSupportedStreamReady: bool - djReady: bool - stemReady: bool - streamStartDate: Optional[datetime] = None - premiumStreamingOnly: bool - trackNumber: int - volumeNumber: int - version: Optional[str] = None - popularity: int - copyright: str - bpm: Optional[int] = None - url: str - isrc: str - editable: bool - explicit: bool - audioQuality: TrackQuality - audioModes: List[str] - mediaMetadata: Dict[str, List[str]] - # for real, artist can be None? - artist: Optional[TrackArtist] = None - artists: List[TrackArtist] - album: TrackAlbum - mixes: Dict[str, str] diff --git a/tiddl/utils.py b/tiddl/utils.py index 852d4a9..b58f886 100644 --- a/tiddl/utils.py +++ b/tiddl/utils.py @@ -6,7 +6,8 @@ from pathlib import Path from typing import Literal, get_args -from tiddl.models import Track, TrackQuality, QUALITY_TO_ARG +from tiddl.models.constants import TrackQuality, QUALITY_TO_ARG +from tiddl.models.resource import Track ResourceTypeLiteral = Literal["track", "album", "playlist", "artist"] @@ -49,7 +50,9 @@ def sanitizeString(string: str) -> str: return re.sub(pattern, "", string) -def formatTrack(template: str, track: Track, album_artist="", playlist_title="", playlist_index=0) -> str: +def formatTrack( + template: str, track: Track, album_artist="", playlist_title="", playlist_index=0 +) -> str: artist = sanitizeString(track.artist.name) if track.artist else "" features = [ sanitizeString(track_artist.name)