Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions streamrip/client/tidal.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import aiohttp

from ..config import Config
from ..exceptions import NonStreamableError
from ..exceptions import ItemNotFoundError, NonStreamableError
from .client import Client
from .downloadable import TidalDownloadable

Expand Down Expand Up @@ -124,7 +124,13 @@ async def get_metadata(self, item_id: str, media_type: str) -> dict:
item["lyrics"] = resp.get("lyrics") or ""
else:
item["lyrics"] = resp.get("subtitles") or resp.get("lyrics") or ""
except TypeError as e:
except ItemNotFoundError:
# Most tracks simply have no lyrics. That is the expected
# answer, not a problem worth reporting.
logger.debug("No lyrics available for track %s", item_id)
except (NonStreamableError, TypeError) as e:
# Lyrics that should have been there but could not be
# fetched -- worth knowing about.
logger.warning(f"Failed to get lyrics for {item_id}: {e}")

logger.debug(item)
Expand Down Expand Up @@ -353,7 +359,10 @@ async def _api_request(self, path: str, params=None, base: str = BASE) -> dict:
async with self.rate_limiter:
async with self.session.get(f"{base}/{path}", params=params) as resp:
if resp.status == 404:
logger.warning("TIDAL: track not found", resp)
raise NonStreamableError("TIDAL: Track not found")
# Logged at debug, not warning: some callers ask for
# optional things (lyrics) where a 404 is the normal
# answer. Callers that do care log it themselves.
logger.debug("TIDAL: item not found (404): %s", resp.url)
raise ItemNotFoundError(f"TIDAL: item not found: {resp.url}")
resp.raise_for_status()
return await resp.json()
9 changes: 9 additions & 0 deletions streamrip/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,14 @@ def print_msg(self, item) -> str:
return " ".join(base_msg)


class ItemNotFoundError(NonStreamableError):
"""The API returned 404 for an item.

A subclass of NonStreamableError so existing handlers are unaffected, but
distinguishable for callers fetching something optional -- "this does not
exist" and "this failed to download" deserve different log levels.
"""


class ConversionError(Exception):
"""ConversionError."""
7 changes: 7 additions & 0 deletions streamrip/rip/parse_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
URL_REGEX = re.compile(
r"https?://(?:www|open|play|listen)?\.?(qobuz|tidal|deezer)\.com?(?:(?:/(album|artist|track|playlist|video|label))|(?:\/[-\w]+?))+\/([-\w]+)",
)
TIDAL_SHARE_SUFFIX_REGEX = re.compile(r"^(https?://[^/]*tidal\.com/.+?)/u/?$")
SOUNDCLOUD_URL_REGEX = re.compile(r"https://soundcloud.com/[-\w:/]+")
LASTFM_URL_REGEX = re.compile(r"https://www.last.fm/user/\w+/playlists/\w+")
QOBUZ_INTERPRETER_URL_REGEX = re.compile(
Expand Down Expand Up @@ -54,6 +55,12 @@ async def into_pending(
class GenericURL(URL):
@classmethod
def from_str(cls, url: str) -> URL | None:
# Tidal's share sheet produces links ending in "/u". URL_REGEX takes
# the last path segment as the item id -- it has to, because Qobuz
# album urls look like /<locale>/album/<slug>/<id> -- so that suffix
# would be parsed as an id of "u" and the API would 404.
url = TIDAL_SHARE_SUFFIX_REGEX.sub(r"\1", url)

generic_url = URL_REGEX.match(url)
if generic_url is None:
return None
Expand Down
22 changes: 22 additions & 0 deletions tests/test_parse_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@ def test_tidal_track_url(self):
self.assertEqual(groups[1], "track") # media_type
self.assertEqual(groups[2], "3083287") # item_id

def test_tidal_share_url_with_u_suffix(self):
"""Test that Tidal share links ending in /u parse to the real item id.

The share sheet appends "/u", which would otherwise be taken as the
item id and 404 against the API.
"""
for url in (
"https://tidal.com/album/152697662/u",
"https://tidal.com/album/152697662/u/",
"https://tidal.com/browse/album/152697662/u",
):
with self.subTest(url=url):
result = parse_url(url)

self.assertIsNotNone(result)
self.assertIsInstance(result, GenericURL)
self.assertEqual(result.source, "tidal")

groups = result.match.groups()
self.assertEqual(groups[1], "album") # media_type
self.assertEqual(groups[2], "152697662") # item_id

def test_deezer_track_url(self):
"""Test that Deezer track URLs are matched correctly."""
url = "https://www.deezer.com/track/4195713"
Expand Down