diff --git a/tiddl/cli/commands/download/__init__.py b/tiddl/cli/commands/download/__init__.py index e43bf63..7afea45 100644 --- a/tiddl/cli/commands/download/__init__.py +++ b/tiddl/cli/commands/download/__init__.py @@ -9,6 +9,7 @@ from rich.live import Live from typing_extensions import Annotated from tiddl.core.metadata import add_track_metadata, add_video_metadata, Cover +from tiddl.core.api import ApiError from tiddl.core.api.models import Album, Track, Video, AlbumItemsCredits from tiddl.core.utils.format import format_template from tiddl.core.utils.m3u import save_tracks_to_m3u @@ -492,7 +493,16 @@ def download_callback( console=ctx.obj.console, transient=True, ): - await asyncio.gather(*(handle_resource(r) for r in ctx.obj.resources)) + + async def wrapper(r: TidalResource): + try: + await handle_resource(r) + except ApiError as e: + ctx.obj.console.print(f"[red]API Error:[/] {e} at {r}") + except Exception as e: + ctx.obj.console.print(f"[red]Error:[/] {e} at {r}") + + await asyncio.gather(*(wrapper(r) for r in ctx.obj.resources)) rich_output.show_stats() diff --git a/tiddl/core/api/api.py b/tiddl/core/api/api.py index a72c741..178f2c3 100644 --- a/tiddl/core/api/api.py +++ b/tiddl/core/api/api.py @@ -34,20 +34,20 @@ ID: TypeAlias = str | int class Limits: # TODO test every max limit - ARTIST_ALBUMS = 50 - ARTIST_ALBUMS_MAX = 200 + ARTIST_ALBUMS = 10 + ARTIST_ALBUMS_MAX = 100 - ARTIST_VIDEOS = 50 - ARTIST_VIDEOS_MAX = 200 + ARTIST_VIDEOS = 10 + ARTIST_VIDEOS_MAX = 100 - ALBUM_ITEMS = 100 + ALBUM_ITEMS = 20 ALBUM_ITEMS_MAX = 100 - PLAYLIST_ITEMS = 50 - PLAYLIST_ITEMS_MAX = 200 + PLAYLIST_ITEMS = 20 + PLAYLIST_ITEMS_MAX = 100 - MIX_ITEMS = 100 - MIX_ITEMS_MAX = 200 + MIX_ITEMS = 20 + MIX_ITEMS_MAX = 100 class TidalAPI: diff --git a/tiddl/core/api/client.py b/tiddl/core/api/client.py index 685163f..7035dc3 100644 --- a/tiddl/core/api/client.py +++ b/tiddl/core/api/client.py @@ -4,6 +4,9 @@ from pathlib import Path from typing import Any, Type, TypeVar from pydantic import BaseModel +from time import sleep + +from requests.exceptions import JSONDecodeError from requests_cache import ( CachedSession, StrOrPath, @@ -15,6 +18,8 @@ from .exceptions import ApiError T = TypeVar("T", bound=BaseModel) API_URL = "https://api.tidal.com/v1" +MAX_RETRIES = 5 +RETRY_DELAY = 2 log = getLogger(__name__) @@ -48,6 +53,7 @@ class TidalClient: endpoint: str, params: dict[str, Any] = {}, expire_after: int = NEVER_EXPIRE, + _attempt: int = 1, ) -> T: """ Fetch data from the API endpoint @@ -62,7 +68,27 @@ class TidalClient: f"{endpoint} {params} '{'HIT' if res.from_cache else 'MISS'}' [{res.status_code}]", ) - data = res.json() + try: + data = res.json() + except JSONDecodeError as e: + if _attempt >= MAX_RETRIES: + log.error(f"JSON decode failed after {MAX_RETRIES} attempts: {e}") + raise ApiError( + status=res.status_code, + subStatus="0", + userMessage="Response body does not contain valid json.", + ) + + log.warning(f"JSON decode error, retrying {_attempt}/{MAX_RETRIES}") + sleep(RETRY_DELAY) + + return self.fetch( + model=model, + endpoint=endpoint, + params=params, + expire_after=expire_after, + _attempt=_attempt + 1, + ) if self.debug_path: file = self.debug_path / f"{endpoint}.json"