Skip to content

Commit 4a0462d

Browse files
authored
Merge pull request #242 from tamland/feature/v0.7.5-prep
Feature/v0.7.5 prep
2 parents 41c31f4 + b4157c2 commit 4a0462d

17 files changed

Lines changed: 720 additions & 537 deletions

README.rst

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,6 @@ Install from `PyPI <https://pypi.python.org/pypi/tidalapi/>`_ using ``pip``:
2020
2121
$ pip install tidalapi
2222
23-
24-
GStreamer
25-
------------
26-
27-
Playback of certain audio qualities
28-
Certain streaming qualities require gstreamer bad-plugins, e.g.:
29-
```
30-
sudo apt-get install gstreamer1.0-plugins-bad
31-
```
32-
This is mandatory to be able to play m4a streams and for playback of mpegdash or hls streams. Otherwise, you will likely get an error:
33-
```
34-
WARNING [MainThread] mopidy.audio.actor Could not find a application/x-hls decoder to handle media.
35-
WARNING [MainThread] mopidy.audio.gst GStreamer warning: No decoder available for type 'application/x-hls'.
36-
ERROR [MainThread] mopidy.audio.gst GStreamer error: Your GStreamer installation is missing a plug-in.
37-
```
38-
39-
4023
Usage
4124
-------------
4225

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# You should have received a copy of the GNU Lesser General Public License
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
#
18-
"""pkce_login.py: A simple example script that describes how to use PKCE login and MPEG-DASH streams"""
18+
"""pkce_example.py: A simple example script that describes how to use PKCE login and MPEG-DASH streams"""
1919

2020
import tidalapi
2121
from tidalapi import Quality
@@ -34,11 +34,13 @@
3434
# HiFi: Quality.high_lossless (FLAC)
3535
# HiFi+ Quality.hi_res (FLAC MQA)
3636
# HiFi+ Quality.hi_res_lossless (FLAC HI_RES)
37-
session.audio_quality = Quality.hi_res_lossless.value
38-
#album_id = "77640617" # U2 / Achtung Baby (Max quality: HI_RES MQA, 16bit/44100Hz)
39-
#album_id = "110827651" # The Black Keys / Let's Rock (Max quality: LOSSLESS FLAC, 24bit/48000Hz)
40-
album_id = "77646169" # Beck / Sea Change (Max quality: HI_RES_LOSSLESS FLAC, 24bit/192000Hz)
37+
session.audio_quality = Quality.hi_res_lossless
38+
# album_id = "249593867" # Alice In Chains / We Die Young (Max quality: HI_RES MHA1 SONY360)
39+
# album_id = "77640617" # U2 / Achtung Baby (Max quality: HI_RES MQA, 16bit/44100Hz)
40+
# album_id = "110827651" # The Black Keys / Let's Rock (Max quality: LOSSLESS FLAC, 24bit/48000Hz)
41+
album_id = "77646169" # Beck / Sea Change (Max quality: HI_RES_LOSSLESS FLAC, 24bit/192000Hz)
4142
album = session.album(album_id)
43+
res = album.get_audio_resolution()
4244
tracks = album.tracks()
4345
# list album tracks
4446
for track in tracks:
@@ -47,11 +49,13 @@
4749
print("MimeType:{}".format(stream.manifest_mime_type))
4850

4951
manifest = stream.get_stream_manifest()
52+
audio_resolution = stream.get_audio_resolution()
53+
5054
print("track:{}, (quality:{}, codec:{}, {}bit/{}Hz)".format(track.id,
51-
stream.audio_quality,
52-
manifest.get_codecs(),
53-
stream.bit_depth,
54-
stream.sample_rate))
55+
stream.audio_quality,
56+
manifest.get_codecs(),
57+
audio_resolution[0],
58+
audio_resolution[1]))
5559
if stream.is_MPD:
5660
# HI_RES_LOSSLESS quality supported when using MPEG-DASH stream (PKCE only!)
5761
# 1. Export as MPD manifest
@@ -65,4 +69,4 @@
6569
elif stream.is_BTS:
6670
# Direct URL (m4a or flac) is available for Quality < HI_RES_LOSSLESS
6771
url = manifest.get_urls()
68-
break
72+
break

examples/simple.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
# HiFi: Quality.high_lossless (FLAC)
3535
# HiFi+ Quality.hi_res (FLAC MQA)
3636
# HiFi+ Quality.hi_res_lossless (FLAC HI_RES)
37-
session.audio_quality = Quality.hi_res_lossless.value
37+
session.audio_quality = Quality.hi_res_lossless
3838

3939
# album_id = "77640617" # U2 / Achtung Baby (Max quality: HI_RES MQA, 16bit/44100Hz)
4040
# album_id = "110827651" # The Black Keys / Let's Rock (Max quality: LOSSLESS FLAC, 24bit/48000Hz)

poetry.lock

Lines changed: 405 additions & 367 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "tidalapi"
33
version = "0.7.5"
44
description = "Unofficial API for TIDAL music streaming service."
55
authors = ["Thomas Amland <thomas.amland@googlemail.com>"]
6-
maintainers = ["tehkillerbee <josaksel.dk@gmail.com>"]
6+
maintainers = ["tehkillerbee <tehkillerbee@users.noreply.github.com>"]
77
license = "LGPL-3.0-or-later"
88
readme = ["README.rst", "HISTORY.rst"]
99
homepage = "https://tidalapi.netlify.app"
@@ -23,7 +23,10 @@ classifiers = [
2323
python = "^3.8"
2424
requests = "^2.28.0"
2525
python-dateutil = "^2.8.2"
26-
typing-extensions = "^4.8.0"
26+
typing-extensions = "^4.10.0"
27+
ratelimit = "^2.2.1"
28+
isodate = "^0.6.1"
29+
mpegdash = "^0.4.0"
2730

2831
[tool.poetry.group.dev.dependencies]
2932
mypy = "^1.3.0"

tests/test_album.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,16 @@ def test_no_release_date(session):
101101
)
102102

103103

104-
def test_default_image_used_if_no_cover_art(mocker):
105-
# TODO find an example if there still are any.
106-
album = Album(mocker.Mock(), None)
107-
assert album.cover is None
108-
assert album.image(1280) == tidalapi.album.DEFAULT_ALBUM_IMAGE
104+
def test_default_image_not_used_on_albums_with_cover_art(session):
105+
album = session.album(108043414)
106+
assert album.cover is not None
107+
default_album_url = "https://resources.tidal.com/images/%s/%ix%i.jpg" % (
108+
tidalapi.album.DEFAULT_ALBUM_IMG.replace("-", "/"),
109+
1280,
110+
1280,
111+
)
112+
# Album should not use default album art
113+
assert album.image(1280) != default_album_url
109114

110115

111116
def test_similar(session):

tests/test_media.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ def test_track_url(session):
6969
def test_lyrics(session):
7070
track = session.track(56480040)
7171
lyrics = track.lyrics()
72-
assert "Think we're there" in lyrics.text
73-
assert "Think we're there" in lyrics.subtitles
72+
assert "I think we're there" in lyrics.text
73+
assert "I think we're there" in lyrics.subtitles
7474
assert lyrics.right_to_left is False
7575

7676

@@ -99,9 +99,9 @@ def test_track_with_album(session):
9999
def test_track_streaming(session):
100100
track = session.track(62392768)
101101
stream = track.get_stream()
102-
assert stream.audio_mode == "STEREO"
102+
assert stream.audio_mode == tidalapi.media.AudioMode.stereo
103103
assert (
104-
stream.audio_quality == tidalapi.Quality.low_320k.value
104+
stream.audio_quality == tidalapi.Quality.low_320k
105105
) # i.e. the default quality for the current session
106106

107107

tests/test_session.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,9 @@ def test_invalid_search(session):
133133
def test_config(session):
134134
assert session.config.item_limit == 1000
135135
assert (
136-
session.config.quality == tidalapi.Quality.low_320k.value
136+
session.config.quality == tidalapi.Quality.low_320k
137137
) # i.e. the default quality for the current session
138-
assert session.config.video_quality == tidalapi.VideoQuality.high.value
138+
assert session.config.video_quality == tidalapi.VideoQuality.high
139139
assert session.config.alac is True
140140

141141

tidalapi/album.py

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
import dateutil.parser
2525

26-
from tidalapi.exceptions import MetadataNotAvailable, ObjectNotFound
26+
from tidalapi.exceptions import MetadataNotAvailable, ObjectNotFound, TooManyRequests
2727
from tidalapi.types import JsonObj
2828

2929
if TYPE_CHECKING:
@@ -33,9 +33,7 @@
3333
from tidalapi.session import Session
3434

3535

36-
DEFAULT_ALBUM_IMAGE = (
37-
"https://tidal.com/browse/assets/images/defaultImages/defaultAlbumImage.png"
38-
)
36+
DEFAULT_ALBUM_IMG = "0dfd3368-3aa1-49a3-935f-10ffb39803c0"
3937

4038

4139
class Album:
@@ -53,6 +51,10 @@ class Album:
5351

5452
duration: Optional[int] = -1
5553
available: Optional[bool] = False
54+
ad_supported_ready: Optional[bool] = False
55+
dj_ready: Optional[bool] = False
56+
allow_streaming: Optional[bool] = False
57+
premium_streaming_only: Optional[bool] = False
5658
num_tracks: Optional[int] = -1
5759
num_videos: Optional[int] = -1
5860
num_volumes: Optional[int] = -1
@@ -64,6 +66,9 @@ class Album:
6466
universal_product_number: Optional[int] = -1
6567
popularity: Optional[int] = -1
6668
user_date_added: Optional[datetime] = None
69+
audio_quality: Optional[str] = ""
70+
audio_modes: Optional[str] = ""
71+
media_metadata_tags: Optional[List[str]] = [""]
6772

6873
artist: Optional["Artist"] = None
6974
artists: Optional[List["Artist"]] = None
@@ -75,9 +80,12 @@ def __init__(self, session: "Session", album_id: Optional[str]):
7580
self.id = album_id
7681

7782
if self.id:
78-
request = self.request.request("GET", "albums/%s" % self.id)
79-
if request.status_code and request.status_code == 404:
83+
try:
84+
request = self.request.request("GET", "albums/%s" % self.id)
85+
except ObjectNotFound:
8086
raise ObjectNotFound("Album not found")
87+
except TooManyRequests:
88+
raise TooManyRequests("Album unavailable")
8189
else:
8290
self.request.map_json(request.json(), parse=self.parse)
8391

@@ -102,6 +110,10 @@ def parse(
102110
self.video_cover = json_obj["videoCover"]
103111
self.duration = json_obj.get("duration")
104112
self.available = json_obj.get("streamReady")
113+
self.ad_supported_ready = json_obj.get("adSupportedStreamReady")
114+
self.dj_ready = json_obj.get("djReady")
115+
self.allow_streaming = json_obj.get("allowStreaming")
116+
self.premium_streaming_only = json_obj.get("premiumStreamingOnly")
105117
self.num_tracks = json_obj.get("numberOfTracks")
106118
self.num_videos = json_obj.get("numberOfVideos")
107119
self.num_volumes = json_obj.get("numberOfVolumes")
@@ -112,6 +124,13 @@ def parse(
112124
self.popularity = json_obj.get("popularity")
113125
self.type = json_obj.get("type")
114126

127+
# Certain fields may not be available
128+
self.audio_quality = json_obj.get("audioQuality")
129+
self.audio_modes = json_obj.get("audioModes")
130+
131+
if "mediaMetadata" in json_obj:
132+
self.media_metadata_tags = json_obj.get("mediaMetadata")["tags"]
133+
115134
self.artist = artist
116135
self.artists = artists
117136

@@ -183,7 +202,7 @@ def items(self, limit: int = 100, offset: int = 0) -> List[Union["Track", "Video
183202
assert isinstance(items, list)
184203
return cast(List[Union["Track", "Video"]], items)
185204

186-
def image(self, dimensions: int = 320, default: str = DEFAULT_ALBUM_IMAGE) -> str:
205+
def image(self, dimensions: int = 320, default: str = DEFAULT_ALBUM_IMG) -> str:
187206
"""A url to an album image cover.
188207
189208
:param dimensions: The width and height that you want from the image
@@ -192,17 +211,22 @@ def image(self, dimensions: int = 320, default: str = DEFAULT_ALBUM_IMAGE) -> st
192211
193212
Valid resolutions: 80x80, 160x160, 320x320, 640x640, 1280x1280
194213
"""
195-
if not self.cover:
196-
return default
197214

198215
if dimensions not in [80, 160, 320, 640, 1280]:
199216
raise ValueError("Invalid resolution {0} x {0}".format(dimensions))
200217

201-
return self.session.config.image_url % (
202-
self.cover.replace("-", "/"),
203-
dimensions,
204-
dimensions,
205-
)
218+
if not self.cover:
219+
return self.session.config.image_url % (
220+
default.replace("-", "/"),
221+
dimensions,
222+
dimensions,
223+
)
224+
else:
225+
return self.session.config.image_url % (
226+
self.cover.replace("-", "/"),
227+
dimensions,
228+
dimensions,
229+
)
206230

207231
def video(self, dimensions: int) -> str:
208232
"""Creates a url to an mp4 video cover for the album.
@@ -239,9 +263,12 @@ def similar(self) -> List["Album"]:
239263
240264
:return: A :any:`list` of similar albums
241265
"""
242-
request = self.request.request("GET", "albums/%s/similar" % self.id)
243-
if request.status_code and request.status_code == 404:
266+
try:
267+
request = self.request.request("GET", "albums/%s/similar" % self.id)
268+
except ObjectNotFound:
244269
raise MetadataNotAvailable("No similar albums exist for this album")
270+
except TooManyRequests:
271+
raise TooManyRequests("Similar artists unavailable")
245272
else:
246273
albums = self.request.map_json(
247274
request.json(), parse=self.session.parse_album
@@ -261,3 +288,23 @@ def review(self) -> str:
261288
]
262289
assert isinstance(review, str)
263290
return review
291+
292+
def get_audio_resolution(self, individual_tracks: bool = False) -> [[int, int]]:
293+
"""Retrieve the audio resolution (bit rate + sample rate) for the album track(s)
294+
295+
This function assumes that all album tracks use the same audio resolution.
296+
Some albums may consist of tracks with multiple audio resolution(s).
297+
The audio resolution can therefore be fetched for individual tracks by setting the `all_tracks` argument accordingly.
298+
299+
WARNING: For individual tracks, many additional requests are needed. Handle with care!
300+
301+
:param individual_tracks: Fetch individual track resolutions
302+
:type individual_tracks: bool
303+
:return: A :class:`tuple` containing the (bit_rate, sample_rate) for one or more tracks
304+
"""
305+
if individual_tracks:
306+
# Return for individual tracks
307+
return [res.get_stream().get_audio_resolution() for res in self.tracks()]
308+
else:
309+
# Return for first track only
310+
return [self.tracks()[0].get_stream().get_audio_resolution()]

tidalapi/artist.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import dateutil.parser
2727
from typing_extensions import NoReturn
2828

29-
from tidalapi.exceptions import ObjectNotFound
29+
from tidalapi.exceptions import ObjectNotFound, TooManyRequests
3030
from tidalapi.types import JsonObj
3131

3232
if TYPE_CHECKING:
@@ -35,6 +35,8 @@
3535
from tidalapi.page import Page
3636
from tidalapi.session import Session
3737

38+
DEFAULT_ARTIST_IMG = "1e01cdb6-f15d-4d8b-8440-a047976c1cac"
39+
3840

3941
class Artist:
4042
id: Optional[str] = None
@@ -54,9 +56,12 @@ def __init__(self, session: "Session", artist_id: Optional[str]):
5456
self.id = artist_id
5557

5658
if self.id:
57-
request = self.request.request("GET", "artists/%s" % self.id)
58-
if request.status_code and request.status_code == 404:
59+
try:
60+
request = self.request.request("GET", "artists/%s" % self.id)
61+
except ObjectNotFound:
5962
raise ObjectNotFound("Artist not found")
63+
except TooManyRequests:
64+
raise TooManyRequests("Artist unavailable")
6065
else:
6166
self.request.map_json(request.json(), parse=self.parse_artist)
6267

@@ -81,7 +86,10 @@ def parse_artist(self, json_obj: JsonObj) -> "Artist":
8186
self.roles = roles
8287
self.role = roles[0]
8388

89+
# Get artist picture or use default
8490
self.picture = json_obj.get("picture")
91+
if self.picture is None:
92+
self.picture = DEFAULT_ARTIST_IMG
8593

8694
user_date_added = json_obj.get("dateAdded")
8795
self.user_date_added = (

0 commit comments

Comments
 (0)