diff --git a/HISTORY.rst b/HISTORY.rst index 66cb3ae..c4cfdd5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,11 @@ History ======= +v0.8.9 +-------- +* Bugfix: Favorite videos default limit incorrect - tehkillerbee_ +* Tests: Added get_favorite_* tests - tehkillerbee_ + v0.8.8 -------- * Bugfix: OAuth Client ID, secret updated - tehkillerbee_ diff --git a/Makefile b/Makefile index 3d701fe..76a5899 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ shell: install: rm -rf dist poetry build - pip install dist/*.whl + pip install dist/*.whl --break-system-packages format: ${POETRY} isort tidalapi tests diff --git a/docs/conf.py b/docs/conf.py index 86b080a..f050bc1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,7 +23,7 @@ author = "The tidalapi Developers" # The full version, including alpha/beta/rc tags -release = "0.8.8" +release = "0.8.9" # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index b1a029c..97fcd5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "tidalapi" -version = "0.8.8" +version = "0.8.9" description = "Unofficial API for TIDAL music streaming service." authors = ["Thomas Amland "] maintainers = ["tehkillerbee "] diff --git a/tests/test_user.py b/tests/test_user.py index c313452..2818e3c 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -417,6 +417,14 @@ def assert_playlists_present(expected_ids: list[str], should_exist: bool): assert_playlists_present(playlists_multiple, should_exist=False) +def test_get_favorite_tracks(session): + favorites = session.user.favorites + tracks = favorites.tracks_paginated() + tracks_count = favorites.get_tracks_count() + assert len(tracks) > 0 # and tracks_count == len(tracks) + assert isinstance(tracks[0], tidalapi.Track) + + def test_add_remove_favorite_track(session): favorites = session.user.favorites track_id = 32961853 @@ -458,17 +466,18 @@ def assert_tracks_present(expected_ids: list[str], should_exist: bool): assert_tracks_present(tracks_multiple, should_exist=False) -def test_add_remove_favorite_video(session): +def test_get_favorite_videos(session): favorites = session.user.favorites - video_id = 160850422 - add_remove(video_id, favorites.add_video, favorites.remove_video, favorites.videos) + videos = favorites.videos_paginated() + videos_count = favorites.get_videos_count() + assert len(videos) == videos_count and videos_count > 0 + assert isinstance(videos[0], tidalapi.media.Video) -def test_get_favorite_mixes(session): +def test_add_remove_favorite_video(session): favorites = session.user.favorites - mixes = favorites.mixes() - assert len(mixes) > 0 - assert isinstance(mixes[0], tidalapi.MixV2) + video_id = 160850422 + add_remove(video_id, favorites.add_video, favorites.remove_video, favorites.videos) def test_get_favorite_playlists_order(session): @@ -517,6 +526,14 @@ def get_playlist_ids(**kwargs) -> list[str]: assert session.user.favorites.remove_playlist(playlist_ids) +def test_get_favorite_albums(session): + favorites = session.user.favorites + albums = favorites.albums_paginated() + albums_count = favorites.get_albums_count() + assert len(albums) > 0 and albums_count == len(albums) + assert isinstance(albums[0], tidalapi.Album) + + def test_get_favorite_albums_order(session): album_ids = [ "446470480", @@ -576,6 +593,13 @@ def get_album_ids(**kwargs) -> list[str]: assert session.user.favorites.remove_album(album_id) +def test_get_favorite_mixes(session): + favorites = session.user.favorites + mixes = favorites.mixes() + assert len(mixes) > 0 + assert isinstance(mixes[0], tidalapi.MixV2) + + def test_get_favorite_mixes_order(session): mix_ids = [ "0007646f7c64d03d56846ed25dae3d", @@ -633,6 +657,14 @@ def get_mix_ids(**kwargs) -> list[str]: assert session.user.favorites.remove_mixes(mix_ids, validate=True) +def test_get_favorite_artists(session): + favorites = session.user.favorites + artists = favorites.artists_paginated() + artists_count = favorites.get_artists_count() + assert len(artists) > 0 and artists_count == len(artists) + assert isinstance(artists[0], tidalapi.Artist) + + def test_get_favorite_artists_order(session): artist_ids = [ "4836523", diff --git a/tidalapi/__init__.py b/tidalapi/__init__.py index b971258..c2cbb69 100644 --- a/tidalapi/__init__.py +++ b/tidalapi/__init__.py @@ -17,4 +17,4 @@ User, ) -__version__ = "0.8.8" +__version__ = "0.8.9" diff --git a/tidalapi/session.py b/tidalapi/session.py index 6e017c0..6fb4d8b 100644 --- a/tidalapi/session.py +++ b/tidalapi/session.py @@ -739,8 +739,9 @@ def token_refresh(self, refresh_token: str) -> bool: request = self.request_session.post(url, params) json = request.json() if request.status_code != 200: - raise AuthenticationError("Authentication failed") - # raise AuthenticationError(Authentication failed json["error"], json["error_description"]) + raise AuthenticationError( + f"Authentication failed with error '{json['error']}: {json['error_description']}'" + ) if not request.ok: log.warning("The refresh token has expired, a new login is required.") return False diff --git a/tidalapi/user.py b/tidalapi/user.py index c99e374..2b514e4 100644 --- a/tidalapi/user.py +++ b/tidalapi/user.py @@ -167,8 +167,8 @@ def public_playlists( ) -> List[Union["Playlist", "UserPlaylist"]]: """Get the (public) playlists created by the user. - :param limit: The index of the first item you want included. - :param offset: The amount of items you want returned. + :param limit: The number of items you want returned. + :param offset: The index of the first item you want included. :return: List of public playlists. """ params = {"limit": limit, "offset": offset} @@ -564,14 +564,14 @@ def artists_paginated( def artists( self, - limit: Optional[int] = None, + limit: int = 50, offset: int = 0, order: Optional[ArtistOrder] = None, order_direction: Optional[OrderDirection] = None, ) -> List["Artist"]: """Get the users favorite artists. - :param limit: Optional; The amount of artists you want returned. + :param limit: The number of artist you want returned. :param offset: The index of the first artist you want included. :param order: Optional; A :class:`ArtistOrder` describing the ordering type when returning the user favorite artists. eg.: "NAME, "DATE" :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC" @@ -625,14 +625,14 @@ def albums_paginated( def albums( self, - limit: Optional[int] = None, + limit: int = 50, offset: int = 0, order: Optional[AlbumOrder] = None, order_direction: Optional[OrderDirection] = None, ) -> List["Album"]: """Get the users favorite albums. - :param limit: Optional; The amount of albums you want returned. + :param limit: The number of albums you want returned. :param offset: The index of the first album you want included. :param order: Optional; A :class:`AlbumOrder` describing the ordering type when returning the user favorite albums. eg.: "NAME, "DATE" :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC" @@ -684,7 +684,7 @@ def playlists_paginated( def playlists( self, - limit: Optional[int] = 50, + limit: int = 50, offset: int = 0, order: Optional[PlaylistOrder] = None, order_direction: Optional[OrderDirection] = None, @@ -692,7 +692,7 @@ def playlists( """Get the users favorite playlists (v2 endpoint), relative to the root folder This function is limited to 50 by TIDAL, requiring pagination. - :param limit: Optional; The number of playlists you want returned (Note: Cannot exceed 50) + :param limit: The number of playlists you want returned (Note: Cannot exceed 50) :param offset: The index of the first playlist to fetch :param order: Optional; A :class:`PlaylistOrder` describing the ordering type when returning the user favorite playlists. eg.: "NAME, "DATE" :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC" @@ -728,7 +728,7 @@ def playlists( def playlist_folders( self, - limit: Optional[int] = 50, + limit: int = 50, offset: int = 0, order: Optional[PlaylistOrder] = None, order_direction: Optional[OrderDirection] = None, @@ -736,7 +736,7 @@ def playlist_folders( ) -> List["Folder"]: """Get a list of folders created by the user. - :param limit: Optional; The number of playlists you want returned (Note: Cannot exceed 50) + :param limit: The number of playlists you want returned (Note: Cannot exceed 50) :param offset: The index of the first playlist folder to fetch :param order: Optional; A :class:`PlaylistOrder` describing the ordering type when returning the user favorite playlists. eg.: "NAME, "DATE" :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC" @@ -797,7 +797,7 @@ def tracks_paginated( :param order: Optional; A :class:`ItemOrder` describing the ordering type when returning the user favorite tracks. eg.: "NAME, "DATE" :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC" - :return: A :class:`list` :class:`~tidalapi.playlist.Playlist` objects containing the favorite tracks. + :return: A :class:`list` :class:`~tidalapi.media.Track` objects containing the favorite tracks. """ count = self.session.user.favorites.get_tracks_count() return get_items( @@ -806,15 +806,15 @@ def tracks_paginated( def tracks( self, - limit: Optional[int] = None, + limit: int = 50, offset: int = 0, order: Optional[ItemOrder] = None, order_direction: Optional[OrderDirection] = None, ) -> List["Track"]: """Get the users favorite tracks. - :param limit: Optional; The amount of items you want returned. - :param offset: The index of the first item you want included. + :param limit: The number of tracks you want returned. + :param offset: The index of the first track you want included. :param order: Optional; A :class:`ItemOrder` describing the ordering type when returning the user favorite tracks. eg.: "NAME, "DATE" :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC" :return: A :class:`list` of :class:`~tidalapi.media.Track` objects containing all of the favorite tracks. @@ -847,16 +847,32 @@ def get_tracks_count( json_obj = self.requests.map_request(f"{self.base_url}/tracks", params=params) return json_obj.get("totalNumberOfItems", 0) + def videos_paginated( + self, + order: Optional[ItemOrder] = None, + order_direction: Optional[OrderDirection] = None, + ) -> List["Video"]: + """Get the users favorite videos, using pagination. + + :param order: Optional; A :class:`ItemOrder` describing the ordering type when returning the user items. eg.: "NAME, "DATE" + :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC" + :return: A :class:`list` :class:`~tidalapi.media.Video` objects containing the favorite videos. + """ + count = self.session.user.favorites.get_videos_count() + return get_items( + self.session.user.favorites.videos, count, order, order_direction + ) + def videos( self, - limit: Optional[int] = None, + limit: int = 50, offset: int = 0, order: Optional[VideoOrder] = None, order_direction: Optional[OrderDirection] = None, ) -> List["Video"]: """Get the users favorite videos. - :param limit: Optional; The amount of videos you want returned. + :param limit: The number of videos you want returned. :param offset: The index of the first video you want included. :param order: Optional; A :class:`VideoOrder` describing the ordering type when returning the user favorite videos. eg.: "NAME, "DATE" :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC" @@ -877,16 +893,31 @@ def videos( ), ) + def get_videos_count( + self, + ) -> int: + """Get the total number of videos in the user's collection. + + This performs a minimal API request (limit=1) to fetch metadata about the tracks + without retrieving all of them. The API response contains 'totalNumberOfItems', + which represents the total items (videos) available. + :return: The number of items available. + """ + params = {"limit": 1, "offset": 0} + + json_obj = self.requests.map_request(f"{self.base_url}/videos", params=params) + return json_obj.get("totalNumberOfItems", 0) + def mixes( self, - limit: Optional[int] = 50, + limit: int = 50, offset: int = 0, order: Optional[MixOrder] = None, order_direction: Optional[OrderDirection] = None, ) -> List["MixV2"]: """Get the users favorite mixes & radio. - :param limit: Optional; The amount of mixes you want returned. + :param limit: The number of mixes you want returned. :param offset: The index of the first mix you want included. :param order: Optional; A :class:`MixOrder` describing the ordering type when returning the user favorite mixes. eg.: "NAME, "DATE" :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC"