Skip to content

Commit 19ca091

Browse files
authored
Merge pull request #366 from semohr/retry-after
Centralized error handling and added after_retry to TooManyRequests error.
2 parents 67581e8 + 09d15d2 commit 19ca091

File tree

8 files changed

+136
-79
lines changed

8 files changed

+136
-79
lines changed

HISTORY.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
33
History
44
=======
5+
6+
Upcoming
7+
--------
8+
* TooManyRequests now includes the retry_after header in its data. - semohr_
9+
* Added a central error class (TidalAPIError) to allow for unified error handling. - semohr_
10+
511
v0.8.6
612
------
713
* Add support for get<track, album, artist, playlist>count(), Workers: Use get_*_count to get the actual number of items. - tehkillerbee_
@@ -242,6 +248,7 @@ v0.6.2
242248
* Add version tag for Track - Husky22_
243249
* Switch to netlify for documentation - morguldir_
244250

251+
.. _semohr: https://github.com/semohr
245252
.. _morguldir: https://github.com/morguldir
246253
.. _Husky22: https://github.com/Husky22
247254
.. _ktnrg45: https://github.com/ktnrg45

tidalapi/album.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,11 @@ def __init__(self, session: "Session", album_id: Optional[str]):
8787
if self.id:
8888
try:
8989
request = self.request.request("GET", "albums/%s" % self.id)
90-
except ObjectNotFound:
91-
raise ObjectNotFound("Album not found")
92-
except TooManyRequests:
93-
raise TooManyRequests("Album unavailable")
90+
except ObjectNotFound as e:
91+
e.args = ("Album with id %s not found" % self.id,)
92+
except TooManyRequests as e:
93+
e.args = ("Album unavailable",)
94+
raise e
9495
else:
9596
self.request.map_json(request.json(), parse=self.parse)
9697

@@ -320,8 +321,9 @@ def similar(self) -> List["Album"]:
320321
request = self.request.request("GET", "albums/%s/similar" % self.id)
321322
except ObjectNotFound:
322323
raise MetadataNotAvailable("No similar albums exist for this album")
323-
except TooManyRequests:
324-
raise TooManyRequests("Similar artists unavailable")
324+
except TooManyRequests as e:
325+
e.args = ("Similar artists unavailable",)
326+
raise e
325327
else:
326328
albums = self.request.map_json(
327329
request.json(), parse=self.session.parse_album

tidalapi/artist.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,12 @@ def __init__(self, session: "Session", artist_id: Optional[str]):
6464
if self.id:
6565
try:
6666
request = self.request.request("GET", "artists/%s" % self.id)
67-
except ObjectNotFound:
68-
raise ObjectNotFound("Artist not found")
69-
except TooManyRequests:
70-
raise TooManyRequests("Artist unavailable")
67+
except ObjectNotFound as e:
68+
e.args = ("Artist with id %s not found" % self.id,)
69+
raise e
70+
except TooManyRequests as e:
71+
e.args = ("Artist unavailable",)
72+
raise e
7173
else:
7274
self.request.map_json(request.json(), parse=self.parse_artist)
7375

@@ -242,8 +244,9 @@ def get_radio(self, limit: int = 100) -> List["Track"]:
242244
)
243245
except ObjectNotFound:
244246
raise MetadataNotAvailable("Track radio not available for this track")
245-
except TooManyRequests:
246-
raise TooManyRequests("Track radio unavailable")
247+
except TooManyRequests as e:
248+
e.args = ("Track radio unavailable",)
249+
raise e
247250
else:
248251
json_obj = request.json()
249252
radio = self.request.map_json(json_obj, parse=self.session.parse_track)
@@ -262,8 +265,9 @@ def get_radio_mix(self) -> mix.Mix:
262265
request = self.request.request("GET", "artists/%s/mix" % self.id)
263266
except ObjectNotFound:
264267
raise MetadataNotAvailable("Artist radio not available for this artist")
265-
except TooManyRequests:
266-
raise TooManyRequests("Artist radio unavailable")
268+
except TooManyRequests as e:
269+
e.args = ("Artist radio unavailable",)
270+
raise e
267271
else:
268272
json_obj = request.json()
269273
return self.session.mix(json_obj.get("id"))

tidalapi/exceptions.py

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,86 @@
1-
class AuthenticationError(Exception):
1+
from __future__ import annotations
2+
3+
import json
4+
import logging
5+
6+
from requests import HTTPError
7+
8+
log = logging.getLogger(__name__)
9+
10+
11+
class TidalAPIError(Exception):
212
pass
313

414

5-
class AssetNotAvailable(Exception):
15+
class AuthenticationError(TidalAPIError):
616
pass
717

818

9-
class TooManyRequests(Exception):
19+
class AssetNotAvailable(TidalAPIError):
1020
pass
1121

1222

13-
class URLNotAvailable(Exception):
23+
class TooManyRequests(TidalAPIError):
24+
retry_after: int
25+
26+
def __init__(self, message: str = "Too many requests", retry_after: int = -1):
27+
super().__init__(message)
28+
self.retry_after = retry_after
29+
30+
31+
class URLNotAvailable(TidalAPIError):
1432
pass
1533

1634

17-
class StreamNotAvailable(Exception):
35+
class StreamNotAvailable(TidalAPIError):
1836
pass
1937

2038

21-
class MetadataNotAvailable(Exception):
39+
class MetadataNotAvailable(TidalAPIError):
2240
pass
2341

2442

25-
class ObjectNotFound(Exception):
43+
class ObjectNotFound(TidalAPIError):
2644
pass
2745

2846

29-
class UnknownManifestFormat(Exception):
47+
class UnknownManifestFormat(TidalAPIError):
3048
pass
3149

3250

33-
class ManifestDecodeError(Exception):
51+
class ManifestDecodeError(TidalAPIError):
3452
pass
3553

3654

37-
class MPDNotAvailableError(Exception):
55+
class MPDNotAvailableError(TidalAPIError):
3856
pass
3957

4058

41-
class InvalidISRC(Exception):
59+
class InvalidISRC(TidalAPIError):
4260
pass
4361

4462

45-
class InvalidUPC(Exception):
63+
class InvalidUPC(TidalAPIError):
4664
pass
65+
66+
67+
def http_error_to_tidal_error(http_error: HTTPError) -> TidalAPIError | None:
68+
response = http_error.response
69+
70+
if response.content:
71+
json_data = response.json()
72+
# Make sure request response contains the detailed error message
73+
if "errors" in json_data:
74+
log.debug("Request response: '%s'", json_data["errors"][0]["detail"])
75+
elif "userMessage" in json_data:
76+
log.debug("Request response: '%s'", json_data["userMessage"])
77+
else:
78+
log.debug("Request response: '%s'", json.dumps(json_data))
79+
80+
elif response.status_code == 404:
81+
return ObjectNotFound("Object not found")
82+
elif response.status_code == 429:
83+
retry_after = int(response.headers.get("Retry-After", -1))
84+
return TooManyRequests("Too many requests", retry_after=retry_after)
85+
86+
return None

tidalapi/media.py

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -329,10 +329,12 @@ def _get(self, media_id: str) -> "Track":
329329

330330
try:
331331
request = self.requests.request("GET", "tracks/%s" % media_id)
332-
except ObjectNotFound:
333-
raise ObjectNotFound("Track not found or unavailable")
334-
except TooManyRequests:
335-
raise TooManyRequests("Track unavailable")
332+
except ObjectNotFound as e:
333+
e.args = ("Track with id %s not found" % media_id,)
334+
raise e
335+
except TooManyRequests as e:
336+
e.args = ("Track unavailable",)
337+
raise e
336338
else:
337339
json_obj = request.json()
338340
track = self.requests.map_json(json_obj, parse=self.parse_track)
@@ -362,8 +364,9 @@ def get_url(self) -> str:
362364
)
363365
except ObjectNotFound:
364366
raise URLNotAvailable("URL not available for this track")
365-
except TooManyRequests:
366-
raise TooManyRequests("URL Unavailable")
367+
except TooManyRequests as e:
368+
e.args = ("URL unavailable",)
369+
raise e
367370
else:
368371
json_obj = request.json()
369372
return cast(str, json_obj["urls"][0])
@@ -378,8 +381,9 @@ def lyrics(self) -> "Lyrics":
378381
request = self.requests.request("GET", "tracks/%s/lyrics" % self.id)
379382
except ObjectNotFound:
380383
raise MetadataNotAvailable("No lyrics exists for this track")
381-
except TooManyRequests:
382-
raise TooManyRequests("Lyrics unavailable")
384+
except TooManyRequests as e:
385+
e.args = ("Lyrics unavailable",)
386+
raise e
383387
else:
384388
json_obj = request.json()
385389
lyrics = self.requests.map_json(json_obj, parse=Lyrics().parse)
@@ -401,8 +405,9 @@ def get_track_radio(self, limit: int = 100) -> List["Track"]:
401405
)
402406
except ObjectNotFound:
403407
raise MetadataNotAvailable("Track radio not available for this track")
404-
except TooManyRequests:
405-
raise TooManyRequests("Track radio unavailable")
408+
except TooManyRequests as e:
409+
e.args = ("Track radio unavailable",)
410+
raise e
406411
else:
407412
json_obj = request.json()
408413
tracks = self.requests.map_json(json_obj, parse=self.session.parse_track)
@@ -420,8 +425,9 @@ def get_radio_mix(self) -> mix.Mix:
420425
request = self.requests.request("GET", "tracks/%s/mix" % self.id)
421426
except ObjectNotFound:
422427
raise MetadataNotAvailable("Track radio not available for this track")
423-
except TooManyRequests:
424-
raise TooManyRequests("Track radio unavailable")
428+
except TooManyRequests as e:
429+
e.args = ("Track radio unavailable",)
430+
raise e
425431
else:
426432
json_obj = request.json()
427433
return self.session.mix(json_obj.get("id"))
@@ -445,8 +451,9 @@ def get_stream(self) -> "Stream":
445451
)
446452
except ObjectNotFound:
447453
raise StreamNotAvailable("Stream not available for this track")
448-
except TooManyRequests:
449-
raise TooManyRequests("Stream unavailable")
454+
except TooManyRequests as e:
455+
e.args = ("Stream unavailable",)
456+
raise e
450457
else:
451458
json_obj = request.json()
452459
stream = self.requests.map_json(json_obj, parse=Stream().parse)
@@ -863,10 +870,12 @@ def _get(self, media_id: str) -> Video:
863870

864871
try:
865872
request = self.requests.request("GET", "videos/%s" % self.id)
866-
except ObjectNotFound:
867-
raise ObjectNotFound("Video not found or unavailable")
868-
except TooManyRequests:
869-
raise TooManyRequests("Video unavailable")
873+
except ObjectNotFound as e:
874+
e.args = ("Video with id %s not found" % media_id,)
875+
raise e
876+
except TooManyRequests as e:
877+
e.args = ("Video unavailable",)
878+
raise e
870879
else:
871880
json_obj = request.json()
872881
video = self.requests.map_json(json_obj, parse=self.parse_video)
@@ -891,8 +900,9 @@ def get_url(self) -> str:
891900
)
892901
except ObjectNotFound:
893902
raise URLNotAvailable("URL not available for this video")
894-
except TooManyRequests:
895-
raise TooManyRequests("URL unavailable)")
903+
except TooManyRequests as e:
904+
e.args = ("URL unavailable",)
905+
raise e
896906
else:
897907
json_obj = request.json()
898908
return cast(str, json_obj["urls"][0])

tidalapi/mix.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,12 @@ def get(self, mix_id: Optional[str] = None) -> "Mix":
9595

9696
try:
9797
request = self.request.request("GET", "pages/mix", params=params)
98-
except ObjectNotFound:
99-
raise ObjectNotFound("Mix not found")
100-
except TooManyRequests:
101-
raise TooManyRequests("Mix unavailable")
98+
except ObjectNotFound as e:
99+
e.args = ("Mix with id %s not found" % mix_id,)
100+
raise e
101+
except TooManyRequests as e:
102+
e.args = ("Mix unavailable",)
103+
raise e
102104
else:
103105
result = self.session.parse_page(request.json())
104106
assert not isinstance(result, list)
@@ -215,10 +217,12 @@ def get(self, mix_id: Optional[str] = None) -> "MixV2":
215217
params = {"mixId": mix_id, "deviceType": "BROWSER"}
216218
try:
217219
request = self.request.request("GET", "pages/mix", params=params)
218-
except ObjectNotFound:
219-
raise ObjectNotFound("Mix not found")
220-
except TooManyRequests:
221-
raise TooManyRequests("Mix unavailable")
220+
except ObjectNotFound as e:
221+
e.args = ("Mix with id %s not found" % mix_id,)
222+
raise e
223+
except TooManyRequests as e:
224+
e.args = ("Mix unavailable",)
225+
raise e
222226
else:
223227
result = self.session.parse_page(request.json())
224228
assert not isinstance(result, list)

tidalapi/playlist.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,12 @@ def __init__(self, session: "Session", playlist_id: Optional[str]):
8181
if playlist_id:
8282
try:
8383
request = self.request.request("GET", self._base_url % self.id)
84-
except ObjectNotFound:
85-
raise ObjectNotFound("Playlist not found")
86-
except TooManyRequests:
87-
raise TooManyRequests("Playlist unavailable")
84+
except ObjectNotFound as e:
85+
e.args = ("Playlist with id %s not found" % playlist_id,)
86+
raise e
87+
except TooManyRequests as e:
88+
e.args = ("Playlist unavailable",)
89+
raise e
8890
else:
8991
self._etag = request.headers["etag"]
9092
self.parse(request.json())
@@ -370,9 +372,10 @@ def __init__(
370372
return
371373
raise ObjectNotFound
372374
except ObjectNotFound:
373-
raise ObjectNotFound(f"Folder not found")
374-
except TooManyRequests:
375-
raise TooManyRequests("Folder unavailable")
375+
raise ObjectNotFound("Folder not found")
376+
except TooManyRequests as e:
377+
e.args = ("Folder unavailable",)
378+
raise e
376379

377380
def _reparse(self) -> None:
378381
params = {

tidalapi/request.py

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
import requests
3636

37-
from tidalapi.exceptions import ObjectNotFound, TooManyRequests
37+
from tidalapi.exceptions import http_error_to_tidal_error
3838
from tidalapi.types import JsonObj
3939

4040
log = logging.getLogger(__name__)
@@ -151,26 +151,13 @@ def request(
151151
log.debug("request: %s", request.request.url)
152152
try:
153153
request.raise_for_status()
154-
except Exception as e:
154+
except requests.HTTPError as e:
155155
log.info("Request resulted in exception {}".format(e))
156156
self.latest_err_response = request
157-
if request.content:
158-
resp = request.json()
159-
# Make sure request response contains the detailed error message
160-
if "errors" in resp:
161-
log.debug("Request response: '%s'", resp["errors"][0]["detail"])
162-
elif "userMessage" in resp:
163-
log.debug("Request response: '%s'", resp["userMessage"])
164-
else:
165-
log.debug("Request response: '%s'", json.dumps(resp))
166-
167-
if request.status_code and request.status_code == 404:
168-
raise ObjectNotFound
169-
elif request.status_code and request.status_code == 429:
170-
raise TooManyRequests
157+
if err := http_error_to_tidal_error(e):
158+
raise err from e
171159
else:
172-
# raise last error, usually HTTPError
173-
raise
160+
raise # re raise last error, usually HTTPError
174161
return request
175162

176163
def get_latest_err_response(self) -> dict:

0 commit comments

Comments
 (0)