From 85cbab9cce38e3cec2089feab6a0598476b68b41 Mon Sep 17 00:00:00 2001 From: ~PV Date: Mon, 10 Jul 2023 22:00:30 -0700 Subject: [PATCH 1/3] Add development info to readme --- README.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.rst b/README.rst index d0265f4e..4ca81be0 100644 --- a/README.rst +++ b/README.rst @@ -48,3 +48,14 @@ Documentation ------------- Documentation is available at https://tidalapi.netlify.app/ + +Development +----------- + +This project uses poetry for dependency management and packaging. To install dependencies and setup the project for development, run: + +.. code-block:: bash + + $ pip install pipx + $ pipx install poetry + $ poetry install --no-root From 54ad6ed48981731e261ba7746d7e5086776ffc68 Mon Sep 17 00:00:00 2001 From: ~PV Date: Mon, 10 Jul 2023 22:28:08 -0700 Subject: [PATCH 2/3] Fix Circular Imports, Additional type checking --- README.rst | 1 + tidalapi/__init__.py | 4 +- tidalapi/media.py | 42 ++++++++++++++------ tidalapi/mix.py | 3 ++ tidalapi/playlist.py | 9 ++++- tidalapi/session.py | 94 +++++++++++++++++++------------------------- tidalapi/user.py | 9 +++-- 7 files changed, 89 insertions(+), 73 deletions(-) diff --git a/README.rst b/README.rst index 4ca81be0..44989bd4 100644 --- a/README.rst +++ b/README.rst @@ -9,6 +9,7 @@ tidalapi Unofficial Python API for TIDAL music streaming service. +Requires Python 3.7 or higher. 0.7.0 Rewrite ------------- diff --git a/tidalapi/__init__.py b/tidalapi/__init__.py index d7a59dc3..e0be07d7 100644 --- a/tidalapi/__init__.py +++ b/tidalapi/__init__.py @@ -1,12 +1,12 @@ from .album import Album # noqa: F401 from .artist import Artist, Role # noqa: F401 from .genre import Genre # noqa: F401 -from .media import Track, Video # noqa: F401 +from .media import Track, Video, Quality, VideoQuality # noqa: F401 from .mix import Mix # noqa: F401 from .page import Page # noqa: F401 from .playlist import Playlist, UserPlaylist # noqa: F401 from .request import Requests # noqa: F401 -from .session import Config, Quality, Session, VideoQuality # noqa: F401 +from .session import Config, Session # noqa: F401 from .user import ( # noqa: F401 Favorites, FetchedUser, diff --git a/tidalapi/media.py b/tidalapi/media.py index 410f4e37..7fbbba0f 100644 --- a/tidalapi/media.py +++ b/tidalapi/media.py @@ -20,17 +20,35 @@ Classes: :class:`Media`, :class:`Track`, :class:`Video` """ +from __future__ import annotations + import copy from abc import abstractmethod from datetime import datetime -from typing import List, Optional, Union, cast +from enum import Enum +from typing import TYPE_CHECKING, List, Optional, Union, cast import dateutil.parser -import tidalapi +if TYPE_CHECKING: + import tidalapi + from tidalapi.types import JsonObj +class Quality(Enum): + lossless = "LOSSLESS" + high = "HIGH" + low = "LOW" + master = "HI_RES" + + +class VideoQuality(Enum): + high = "HIGH" + medium = "MEDIUM" + low = "LOW" + + class Media: """ Base class for generic media, specifically :class:`Track` and :class:`Video` @@ -52,16 +70,14 @@ class Media: volume_num: int = 1 explicit: bool = False popularity: int = -1 - artist: Optional[tidalapi.artist.Artist] = None + artist: Optional[tidalapi.Artist] = None #: For the artist credit page artist_roles = None - artists: Optional[List[tidalapi.artist.Artist]] = None + artists: Optional[List[tidalapi.Artist]] = None album: Optional[tidalapi.album.Album] = None type: Optional[str] = None - def __init__( - self, session: tidalapi.session.Session, media_id: Optional[str] = None - ): + def __init__(self, session: tidalapi.Session, media_id: Optional[str] = None): self.session = session self.requests = self.session.request self.album = session.album() @@ -70,7 +86,7 @@ def __init__( self._get(self.id) @abstractmethod - def _get(self, media_id: str) -> "Media": + def _get(self, media_id: str) -> Media: raise NotImplementedError( "You are not supposed to use the media class directly." ) @@ -140,12 +156,12 @@ class Track(Media): replay_gain = None peak = None isrc = None - audio_quality: Optional[tidalapi.session.Quality] = None + audio_quality: Optional[Quality] = None version = None full_name: Optional[str] = None copyright = None - def parse_track(self, json_obj: JsonObj) -> "Track": + def parse_track(self, json_obj: JsonObj) -> Track: Media.parse(self, json_obj) self.replay_gain = json_obj["replayGain"] # Tracks from the pages endpoints might not actually exist @@ -153,7 +169,7 @@ def parse_track(self, json_obj: JsonObj) -> "Track": self.peak = json_obj["peak"] self.isrc = json_obj["isrc"] self.copyright = json_obj["copyright"] - self.audio_quality = tidalapi.session.Quality(json_obj["audioQuality"]) + self.audio_quality = Quality(json_obj["audioQuality"]) self.version = json_obj["version"] if self.version is not None: @@ -284,7 +300,7 @@ class Video(Media): video_quality: Optional[str] = None cover: Optional[str] = None - def parse_video(self, json_obj: JsonObj) -> "Video": + def parse_video(self, json_obj: JsonObj) -> Video: Media.parse(self, json_obj) release_date = json_obj.get("releaseDate") self.release_date = ( @@ -296,7 +312,7 @@ def parse_video(self, json_obj: JsonObj) -> "Video": return copy.copy(self) - def _get(self, media_id: str) -> "Video": + def _get(self, media_id: str) -> Video: """Returns information about the video, and replaces the object used to call this function. diff --git a/tidalapi/mix.py b/tidalapi/mix.py index 4d7eeab6..96f23f5c 100644 --- a/tidalapi/mix.py +++ b/tidalapi/mix.py @@ -15,6 +15,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . """A module containing functions relating to TIDAL mixes.""" + +from __future__ import annotations + import copy from enum import Enum from typing import TYPE_CHECKING, List, Optional, TypedDict, Union diff --git a/tidalapi/playlist.py b/tidalapi/playlist.py index 4837f519..564e5b57 100644 --- a/tidalapi/playlist.py +++ b/tidalapi/playlist.py @@ -17,8 +17,13 @@ # along with this program. If not, see . """A module containing things related to TIDAL playlists.""" +from __future__ import annotations + import copy -from typing import Optional +from typing import TYPE_CHECKING, List, Optional + +if TYPE_CHECKING: + import tidalapi import dateutil.parser @@ -118,7 +123,7 @@ def parse_factory(self, json_obj): self.parse(json_obj) return copy.copy(self.factory()) - def tracks(self, limit=None, offset=0): + def tracks(self, limit: Optional[int] = None, offset=0) -> List[tidalapi.Track]: """Gets the playlists̈́' tracks from TIDAL. :param limit: The amount of items you want returned. diff --git a/tidalapi/session.py b/tidalapi/session.py index 15802fe1..5aa30bff 100644 --- a/tidalapi/session.py +++ b/tidalapi/session.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -from __future__ import print_function, unicode_literals +from __future__ import annotations, print_function, unicode_literals import base64 import concurrent.futures @@ -27,6 +27,7 @@ import uuid from enum import Enum from typing import ( + TYPE_CHECKING, Any, Callable, List, @@ -41,39 +42,22 @@ import requests -import tidalapi.album -import tidalapi.artist -import tidalapi.genre -import tidalapi.media -import tidalapi.mix -import tidalapi.playlist -import tidalapi.request -import tidalapi.user +if TYPE_CHECKING: + import tidalapi + +from . import album, artist, genre, media, mix, page, playlist, request, user log = logging.getLogger("__NAME__") SearchTypes: List[Optional[Any]] = [ - tidalapi.artist.Artist, - tidalapi.album.Album, - tidalapi.media.Track, - tidalapi.media.Video, - tidalapi.playlist.Playlist, + artist.Artist, + album.Album, + media.Track, + media.Video, + playlist.Playlist, None, ] -class Quality(Enum): - lossless = "LOSSLESS" - high = "HIGH" - low = "LOW" - master = "HI_RES" - - -class VideoQuality(Enum): - high = "HIGH" - medium = "MEDIUM" - low = "LOW" - - class LinkLogin(object): """The data required for logging in to TIDAL using a remote link, json is the data returned from TIDAL.""" @@ -107,8 +91,8 @@ class Config(object): @no_type_check def __init__( self, - quality=Quality.high, - video_quality=VideoQuality.high, + quality=media.Quality.high, + video_quality=media.VideoQuality.high, item_limit=1000, alac=True, ): @@ -222,15 +206,15 @@ class Session(object): session_id = None country_code = None #: A :class:`.User` object containing the currently logged in user. - user = None + user: Union[user.FetchedUser, user.LoggedInUser, user.PlaylistCreator] = None def __init__(self, config=Config()): self.config = config self.request_session = requests.Session() # Objects for keeping the session across all modules. - self.request = tidalapi.Requests(session=self) - self.genre = tidalapi.Genre(session=self) + self.request = request.Requests(session=self) + self.genre = genre.Genre(session=self) self.parse_album = self.album().parse self.parse_artist = self.artist().parse_artist @@ -242,8 +226,8 @@ def __init__(self, config=Config()): self.parse_media = self.track().parse_media self.parse_mix = self.mix().parse - self.parse_user = tidalapi.User(self, None).parse - self.page = tidalapi.Page(self, None) + self.parse_user = user.User(self, None).parse + self.page = page.Page(self, None) self.parse_page = self.page.parse # Dictionary to convert between models from this library, to the text they, and to the parsing function. @@ -319,7 +303,7 @@ def load_session(self, session_id, country_code=None, user_id=None): user_id = request["userId"] self.country_code = country_code - self.user = tidalapi.User(self, user_id=user_id).factory() + self.user = user.User(self, user_id=user_id).factory() return True def load_oauth_session( @@ -347,7 +331,7 @@ def load_oauth_session( self.session_id = json["sessionId"] self.country_code = json["countryCode"] - self.user = tidalapi.User(self, user_id=json["userId"]).factory() + self.user = user.User(self, user_id=json["userId"]).factory() return True @@ -374,7 +358,7 @@ def login(self, username, password): body = request.json() self.session_id = body["sessionId"] self.country_code = body["countryCode"] - self.user = tidalapi.User(self, user_id=body["userId"]).factory() + self.user = user.User(self, user_id=body["userId"]).factory() return True def login_oauth_simple(self, function=print): @@ -385,7 +369,7 @@ def login_oauth_simple(self, function=print): :raises: TimeoutError: If the login takes too long """ login, future = self.login_oauth() - text = "Visit {0} to log in, the code will expire in {1} seconds" + text = "Visit https://{0} to log in, the code will expire in {1} seconds" function(text.format(login.verification_uri_complete, login.expires_in)) future.result() @@ -427,7 +411,7 @@ def _process_link_login(self, json): json = session.json() self.session_id = json["sessionId"] self.country_code = json["countryCode"] - self.user = tidalapi.User(self, user_id=json["userId"]).factory() + self.user = user.User(self, user_id=json["userId"]).factory() def _wait_for_link_login(self, json): expiry = json["expiresIn"] @@ -544,7 +528,9 @@ def check_login(self): "GET", "users/%s/subscription" % self.user.id ).ok - def playlist(self, playlist_id=None): + def playlist( + self, playlist_id=None + ) -> Union[tidalapi.Playlist, tidalapi.UserPlaylist]: """Function to create a playlist object with access to the session instance in a smoother way. Calls :class:`tidalapi.Playlist(session=session, playlist_id=playlist_id) <.Playlist>` internally. @@ -553,9 +539,9 @@ def playlist(self, playlist_id=None): :return: Returns a :class:`.Playlist` object that has access to the session instance used. """ - return tidalapi.Playlist(session=self, playlist_id=playlist_id).factory() + return playlist.Playlist(session=self, playlist_id=playlist_id).factory() - def track(self, track_id=None, with_album=False): + def track(self, track_id=None, with_album=False) -> tidalapi.Track: """Function to create a Track object with access to the session instance in a smoother way. Calls :class:`tidalapi.Track(session=session, track_id=track_id) <.Track>` internally. @@ -565,7 +551,7 @@ def track(self, track_id=None, with_album=False): :return: Returns a :class:`.Track` object that has access to the session instance used. """ - item = tidalapi.Track(session=self, media_id=track_id) + item = media.Track(session=self, media_id=track_id) if item.album and with_album: album = self.album(item.album.id) if album: @@ -573,7 +559,7 @@ def track(self, track_id=None, with_album=False): return item - def video(self, video_id=None): + def video(self, video_id=None) -> tidalapi.Video: """Function to create a Video object with access to the session instance in a smoother way. Calls :class:`tidalapi.Video(session=session, video_id=video_id) <.Video>` internally. @@ -582,9 +568,9 @@ def video(self, video_id=None): :return: Returns a :class:`.Video` object that has access to the session instance used. """ - return tidalapi.Video(session=self, media_id=video_id) + return media.Video(session=self, media_id=video_id) - def artist(self, artist_id: Optional[str] = None) -> tidalapi.artist.Artist: + def artist(self, artist_id: Optional[str] = None) -> tidalapi.Artist: """Function to create a Artist object with access to the session instance in a smoother way. Calls :class:`tidalapi.Artist(session=session, artist_id=artist_id) <.Artist>` internally. @@ -593,7 +579,7 @@ def artist(self, artist_id: Optional[str] = None) -> tidalapi.artist.Artist: :return: Returns a :class:`.Artist` object that has access to the session instance used. """ - return tidalapi.Artist(session=self, artist_id=artist_id) + return artist.Artist(session=self, artist_id=artist_id) def album(self, album_id: Optional[str] = None) -> tidalapi.Album: """Function to create a Album object with access to the session instance in a @@ -604,9 +590,9 @@ def album(self, album_id: Optional[str] = None) -> tidalapi.Album: :return: Returns a :class:`.Album` object that has access to the session instance used. """ - return tidalapi.Album(session=self, album_id=album_id) + return album.Album(session=self, album_id=album_id) - def mix(self, mix_id=None): + def mix(self, mix_id=None) -> tidalapi.Mix: """Function to create a mix object with access to the session instance smoothly Calls :class:`tidalapi.Mix(session=session, mix_id=mix_id) <.Album>` internally. @@ -614,18 +600,20 @@ def mix(self, mix_id=None): :return: Returns a :class:`.Mix` object that has access to the session instance used. """ - return tidalapi.Mix(session=self, mix_id=mix_id) + return mix.Mix(session=self, mix_id=mix_id) - def get_user(self, user_id=None): + def get_user( + self, user_id=None + ) -> Union[tidalapi.FetchedUser, tidalapi.LoggedInUser, tidalapi.PlaylistCreator]: """Function to create a User object with access to the session instance in a - smoother way. Calls :class:`tidalapi.User(session=session, user_id=user_id) + smoother way. Calls :class:`user.User(session=session, user_id=user_id) <.User>` internally. :param user_id: (Optional) The TIDAL id of the User. You may want access to the methods without an id. :return: Returns a :class:`.User` object that has access to the session instance used. """ - return tidalapi.User(session=self, user_id=user_id).factory() + return user.User(session=self, user_id=user_id).factory() def home(self): """ diff --git a/tidalapi/user.py b/tidalapi/user.py index 39b8c45e..ba645336 100644 --- a/tidalapi/user.py +++ b/tidalapi/user.py @@ -21,10 +21,13 @@ :class:`Favorites` is class with a users favorites. """ +from __future__ import annotations + from copy import copy -from typing import Dict, Optional, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Union -import dateutil.parser +if TYPE_CHECKING: + from . import playlist class User(object): @@ -112,7 +115,7 @@ def parse(self, json_obj): return copy(self) - def playlists(self): + def playlists(self) -> List[Union[playlist.Playlist, playlist.UserPlaylist]]: """Get the playlists created by the user. :return: Returns a list of :class:`~tidalapi.playlist.Playlist` objects containing the playlists. From df05d6060d84ac91d57cad87641d26ed7e72eade Mon Sep 17 00:00:00 2001 From: John Maximilian <2e0byo@gmail.com> Date: Wed, 12 Jul 2023 20:05:25 +0100 Subject: [PATCH 3/3] chore: lint --- tidalapi/__init__.py | 2 +- tidalapi/session.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tidalapi/__init__.py b/tidalapi/__init__.py index e0be07d7..ea423dca 100644 --- a/tidalapi/__init__.py +++ b/tidalapi/__init__.py @@ -1,7 +1,7 @@ from .album import Album # noqa: F401 from .artist import Artist, Role # noqa: F401 from .genre import Genre # noqa: F401 -from .media import Track, Video, Quality, VideoQuality # noqa: F401 +from .media import Quality, Track, Video, VideoQuality # noqa: F401 from .mix import Mix # noqa: F401 from .page import Page # noqa: F401 from .playlist import Playlist, UserPlaylist # noqa: F401 diff --git a/tidalapi/session.py b/tidalapi/session.py index 5aa30bff..e3631119 100644 --- a/tidalapi/session.py +++ b/tidalapi/session.py @@ -606,8 +606,8 @@ def get_user( self, user_id=None ) -> Union[tidalapi.FetchedUser, tidalapi.LoggedInUser, tidalapi.PlaylistCreator]: """Function to create a User object with access to the session instance in a - smoother way. Calls :class:`user.User(session=session, user_id=user_id) - <.User>` internally. + smoother way. Calls :class:`user.User(session=session, user_id=user_id) <.User>` + internally. :param user_id: (Optional) The TIDAL id of the User. You may want access to the methods without an id. :return: Returns a :class:`.User` object that has access to the session instance used.