diff --git a/README.rst b/README.rst
index d0265f4e..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
-------------
@@ -48,3 +49,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
diff --git a/tidalapi/__init__.py b/tidalapi/__init__.py
index d7a59dc3..ea423dca 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 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
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..e3631119 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)
- <.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.
"""
- 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.