Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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_
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <thomas.amland@googlemail.com>"]
maintainers = ["tehkillerbee <tehkillerbee@users.noreply.github.com>"]
Expand Down
46 changes: 39 additions & 7 deletions tests/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion tidalapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
User,
)

__version__ = "0.8.8"
__version__ = "0.8.9"
5 changes: 3 additions & 2 deletions tidalapi/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 49 additions & 18 deletions tidalapi/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -684,15 +684,15 @@ 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,
) -> List["Playlist"]:
"""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"
Expand Down Expand Up @@ -728,15 +728,15 @@ 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,
parent_folder_id: str = "root",
) -> 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"
Expand Down Expand Up @@ -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(
Expand All @@ -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.
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down