streamrip
streamrip copied to clipboard
[BUG] some tidal track url is not being detected, `/u` suffix
Describe the bug
when a tidal url has the /u suffix it messes the detection. I get this url by right clicking a song on tidal and then clicking copy, so standard procedure. I think it so common we should support it
I plan to make a PR
$ rip url 'https://tidal.com/track/53796003/u'
...
TypeError: not all arguments converted during string formatting
Command Used
rip url 'https://tidal.com/track/53796003/u'
Debug Traceback
$ rip -vvv url 'https://tidal.com/track/53796003/u'
[20:50:59] DEBUG Showing all debug logs cli.py:111
DEBUG Executing SELECT EXISTS(SELECT 1 FROM downloads WHERE id=?) db.py:108
DEBUG Removing dirs set() artwork.py:19
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /home/ivan/.local/bin/rip:10 in <module> │
│ │
│ 7 │ │ sys.argv[0] = sys.argv[0][:-11] │
│ 8 │ elif sys.argv[0].endswith(".exe"): │
│ 9 │ │ sys.argv[0] = sys.argv[0][:-4] │
│ ❱ 10 │ sys.exit(rip()) │
│ 11 │
│ │
│ ╭──────────── locals ─────────────╮ │
│ │ rip = <HelpColorsGroup rip> │ │
│ │ sys = <module 'sys' (built-in)> │ │
│ ╰─────────────────────────────────╯ │
│ │
│ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/click/core.py:1462 in │
│ __call__ │
│ │
│ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/click/core.py:1383 in │
│ main │
│ │
│ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/click/core.py:1850 in │
│ invoke │
│ │
│ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/click/core.py:1246 in │
│ invoke │
│ │
│ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/click/core.py:814 in │
│ invoke │
│ │
│ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/click/decorators.py:34 │
│ in new_func │
│ │
│ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/streamrip/rip/cli.py:29 │
│ in wrapper │
│ │
│ 26 def coro(f): │
│ 27 │ @wraps(f) │
│ 28 │ def wrapper(*args, **kwargs): │
│ ❱ 29 │ │ return asyncio.run(f(*args, **kwargs)) │
│ 30 │ │
│ 31 │ return wrapper │
│ 32 │
│ │
│ ╭────────────────────────── locals ──────────────────────────╮ │
│ │ args = (<click.core.Context object at 0x7f035615efd0>,) │ │
│ │ kwargs = {'urls': ('https://tidal.com/track/53796003/u',)} │ │
│ ╰────────────────────────────────────────────────────────────╯ │
│ │
│ /usr/lib/python3.13/asyncio/runners.py:195 in run │
│ │
│ 192 │ │ │ "asyncio.run() cannot be called from a running event loop") │
│ 193 │ │
│ 194 │ with Runner(debug=debug, loop_factory=loop_factory) as runner: │
│ ❱ 195 │ │ return runner.run(main) │
│ 196 │
│ 197 │
│ 198 def _cancel_all_tasks(loop): │
│ │
│ ╭───────────────────────────── locals ─────────────────────────────╮ │
│ │ debug = None │ │
│ │ loop_factory = None │ │
│ │ main = <coroutine object url at 0x7f03566aed60> │ │
│ │ runner = <asyncio.runners.Runner object at 0x7f0356667b60> │ │
│ ╰──────────────────────────────────────────────────────────────────╯ │
│ │
│ /usr/lib/python3.13/asyncio/runners.py:118 in run │
│ │
│ 115 │ │ │
│ 116 │ │ self._interrupt_count = 0 │
│ 117 │ │ try: │
│ ❱ 118 │ │ │ return self._loop.run_until_complete(task) │
│ 119 │ │ except exceptions.CancelledError: │
│ 120 │ │ │ if self._interrupt_count > 0: │
│ 121 │ │ │ │ uncancel = getattr(task, "uncancel", None) │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ context = <_contextvars.Context object at 0x7f03566f2a80> │ │
│ │ coro = <coroutine object url at 0x7f03566aed60> │ │
│ │ self = <asyncio.runners.Runner object at 0x7f0356667b60> │ │
│ │ sigint_handler = functools.partial(<bound method Runner._on_sigint of │ │
│ │ <asyncio.runners.Runner object at 0x7f0356667b60>>, main_task=<Task │ │
│ │ finished name='Task-1' coro=<url() done, defined at │ │
│ │ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/st… │ │
│ │ exception=TypeError('not all arguments converted during string │ │
│ │ formatting')>) │ │
│ │ task = <Task finished name='Task-1' coro=<url() done, defined at │ │
│ │ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/st… │ │
│ │ exception=TypeError('not all arguments converted during string │ │
│ │ formatting')> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /usr/lib/python3.13/asyncio/base_events.py:725 in run_until_complete │
│ │
│ 722 │ │ if not future.done(): │
│ 723 │ │ │ raise RuntimeError('Event loop stopped before Future completed.') │
│ 724 │ │ │
│ ❱ 725 │ │ return future.result() │
│ 726 │ │
│ 727 │ def stop(self): │
│ 728 │ │ """Stop running the event loop. │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ future = <Task finished name='Task-1' coro=<url() done, defined at │ │
│ │ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/streamri… │ │
│ │ exception=TypeError('not all arguments converted during string formatting')> │ │
│ │ new_task = False │ │
│ │ self = <_UnixSelectorEventLoop running=False closed=True debug=False> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/streamrip/rip/cli.py:192 │
│ in url │
│ │
│ 189 │ │ │ │
│ 190 │ │ │ async with Main(cfg) as main: │
│ 191 │ │ │ │ await main.add_all(urls) │
│ ❱ 192 │ │ │ │ await main.resolve() │
│ 193 │ │ │ │ await main.rip() │
│ 194 │ │ │ │
│ 195 │ │ │ if version_coro is not None: │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ cfg = <streamrip.config.Config object at 0x7f0356667770> │ │
│ │ ctx = <click.core.Context object at 0x7f035615efd0> │ │
│ │ main = <streamrip.rip.main.Main object at 0x7f0356170d70> │ │
│ │ updates = True │ │
│ │ urls = ('https://tidal.com/track/53796003/u',) │ │
│ │ version_coro = <Task finished name='Task-2' coro=<latest_streamrip_version() done, defined │ │
│ │ at │ │
│ │ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/stre… │ │
│ │ result=('2.1.0', None)> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/streamrip/rip/main.py:16 │
│ 0 in resolve │
│ │
│ 157 │ │ with console.status("Resolving URLs...", spinner="dots"): │
│ 158 │ │ │ coros = [p.resolve() for p in self.pending] │
│ 159 │ │ │ new_media: list[Media] = [ │
│ ❱ 160 │ │ │ │ m for m in await asyncio.gather(*coros) if m is not None │
│ 161 │ │ │ ] │
│ 162 │ │ │
│ 163 │ │ self.media.extend(new_media) │
│ │
│ ╭─────────────────────────────── locals ───────────────────────────────╮ │
│ │ coros = [<coroutine object PendingSingle.resolve at 0x7f03566678b0>] │ │
│ │ self = <streamrip.rip.main.Main object at 0x7f0356170d70> │ │
│ ╰──────────────────────────────────────────────────────────────────────╯ │
│ │
│ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/streamrip/media/track.py │
│ :195 in resolve │
│ │
│ 192 │ │ │ return None │
│ 193 │ │ │
│ 194 │ │ try: │
│ ❱ 195 │ │ │ resp = await self.client.get_metadata(self.id, "track") │
│ 196 │ │ except NonStreamableError as e: │
│ 197 │ │ │ logger.error(f"Error fetching track {self.id}: {e}") │
│ 198 │ │ │ return None │
│ │
│ ╭───────────────────────────────────── locals ─────────────────────────────────────╮ │
│ │ self = PendingSingle( │ │
│ │ │ id='u', │ │
│ │ │ client=<streamrip.client.tidal.TidalClient object at 0x7f0356171010>, │ │
│ │ │ config=<streamrip.config.Config object at 0x7f0356667770>, │ │
│ │ │ db=Database( │ │
│ │ │ │ downloads=<streamrip.db.Downloads object at 0x7f0356171e80>, │ │
│ │ │ │ failed=<streamrip.db.Failed object at 0x7f0356171fd0> │ │
│ │ │ ) │ │
│ │ ) │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/streamrip/client/tidal.p │
│ y:90 in get_metadata │
│ │
│ 87 │ │ ), media_type │
│ 88 │ │ │
│ 89 │ │ url = f"{media_type}s/{item_id}" │
│ ❱ 90 │ │ item = await self._api_request(url) │
│ 91 │ │ if media_type in ("playlist", "album"): │
│ 92 │ │ │ # TODO: move into new method and make concurrent │
│ 93 │ │ │ resp = await self._api_request(f"{url}/items") │
│ │
│ ╭────────────────────────────────── locals ──────────────────────────────────╮ │
│ │ item_id = 'u' │ │
│ │ media_type = 'track' │ │
│ │ self = <streamrip.client.tidal.TidalClient object at 0x7f0356171010> │ │
│ │ url = 'tracks/u' │ │
│ ╰────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/streamrip/client/tidal.p │
│ y:358 in _api_request │
│ │
│ 355 │ │ async with self.rate_limiter: │
│ 356 │ │ │ async with self.session.get(f"{base}/{path}", params=params) as resp: │
│ 357 │ │ │ │ if resp.status == 404: │
│ ❱ 358 │ │ │ │ │ logger.warning("TIDAL: track not found", resp) │
│ 359 │ │ │ │ │ raise NonStreamableError("TIDAL: Track not found") │
│ 360 │ │ │ │ resp.raise_for_status() │
│ 361 │ │ │ │ return await resp.json() │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ base = 'https://api.tidalhifi.com/v1' │ │
│ │ params = {'countryCode': 'MX', 'limit': 100} │ │
│ │ path = 'tracks/u' │ │
│ │ resp = <ClientResponse(https://api.tidalhifi.com/v1/tracks/u?countryCode=MX&limit=100) │ │
│ │ [404 Not Found]> │ │
│ │ <CIMultiDictProxy('Content-Type': 'application/json', 'Content-Length': '66', │ │
│ │ 'Connection': 'keep-alive', 'Date': 'Sun, 16 Nov 2025 02:50:59 GMT', 'Server': │ │
│ │ 'noyb', 'Cache-Control': 'no-cache', 'X-Cache': 'Error from cloudfront', 'Via': │ │
│ │ '1.1 REDACTED.cloudfront.net (CloudFront)', 'X-Amz-Cf-Pop': │ │
│ │ 'REDACTED', 'X-Amz-Cf-Id': │ │
│ │ '64-REDACTED==')> │ │
│ │ self = <streamrip.client.tidal.TidalClient object at 0x7f0356171010> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /usr/lib/python3.13/logging/__init__.py:1532 in warning │
│ │
│ 1529 │ │ logger.warning("Houston, we have a %s", "bit of a problem", exc_info=True) │
│ 1530 │ │ """ │
│ 1531 │ │ if self.isEnabledFor(WARNING): │
│ ❱ 1532 │ │ │ self._log(WARNING, msg, args, **kwargs) │
│ 1533 │ │
│ 1534 │ def warn(self, msg, *args, **kwargs): │
│ 1535 │ │ warnings.warn("The 'warn' method is deprecated, " │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ args = ( │ │
│ │ │ <ClientResponse(https://api.tidalhifi.com/v1/tracks/u?countryCode=MX&limit=100) │ │
│ │ [404 Not Found]> │ │
│ │ <CIMultiDictProxy('Content-Type': 'application/json', 'Content-Length': '66', │ │
│ │ 'Connection': 'keep-alive', 'Date': 'Sun, 16 Nov 2025 02:50:59 GMT', 'Server': │ │
│ │ 'noyb', 'Cache-Control': 'no-cache', 'X-Cache': 'Error from cloudfront', 'Via': │ │
│ │ '1.1 REDACTED.cloudfront.net (CloudFront)', 'X-Amz-Cf-Pop': │ │
│ │ 'REDACTED', 'X-Amz-Cf-Id': │ │
│ │ '64-REDACTED==')> │ │
│ │ , │ │
│ │ ) │ │
│ │ kwargs = {} │ │
│ │ msg = 'TIDAL: track not found' │ │
│ │ self = <Logger streamrip (DEBUG)> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /usr/lib/python3.13/logging/__init__.py:1665 in _log │
│ │
│ 1662 │ │ │ │ exc_info = sys.exc_info() │
│ 1663 │ │ record = self.makeRecord(self.name, level, fn, lno, msg, args, │
│ 1664 │ │ │ │ │ │ │ │ exc_info, func, extra, sinfo) │
│ ❱ 1665 │ │ self.handle(record) │
│ 1666 │ │
│ 1667 │ def handle(self, record): │
│ 1668 │ │ """ │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ args = ( │ │
│ │ │ │ │
│ │ <ClientResponse(https://api.tidalhifi.com/v1/tracks/u?countryCode=MX&limit=100) │ │
│ │ [404 Not Found]> │ │
│ │ <CIMultiDictProxy('Content-Type': 'application/json', 'Content-Length': '66', │ │
│ │ 'Connection': 'keep-alive', 'Date': 'Sun, 16 Nov 2025 02:50:59 GMT', 'Server': │ │
│ │ 'noyb', 'Cache-Control': 'no-cache', 'X-Cache': 'Error from cloudfront', 'Via': │ │
│ │ '1.1 REDACTED.cloudfront.net (CloudFront)', │ │
│ │ 'X-Amz-Cf-Pop': 'REDACTED', 'X-Amz-Cf-Id': │ │
│ │ '64-REDACTED==')> │ │
│ │ , │ │
│ │ ) │ │
│ │ exc_info = None │ │
│ │ extra = None │ │
│ │ fn = '/home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/strea… │ │
│ │ func = '_api_request' │ │
│ │ level = 30 │ │
│ │ lno = 358 │ │
│ │ msg = 'TIDAL: track not found' │ │
│ │ record = <LogRecord: streamrip, 30, │ │
│ │ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/stream… │ │
│ │ 358, "TIDAL: track not found"> │ │
│ │ self = <Logger streamrip (DEBUG)> │ │
│ │ sinfo = None │ │
│ │ stack_info = False │ │
│ │ stacklevel = 1 │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /usr/lib/python3.13/logging/__init__.py:1681 in handle │
│ │
│ 1678 │ │ │ return │
│ 1679 │ │ if isinstance(maybe_record, LogRecord): │
│ 1680 │ │ │ record = maybe_record │
│ ❱ 1681 │ │ self.callHandlers(record) │
│ 1682 │ │
│ 1683 │ def addHandler(self, hdlr): │
│ 1684 │ │ """ │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ maybe_record = <LogRecord: streamrip, 30, │ │
│ │ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/stre… │ │
│ │ 358, "TIDAL: track not found"> │ │
│ │ record = <LogRecord: streamrip, 30, │ │
│ │ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/stre… │ │
│ │ 358, "TIDAL: track not found"> │ │
│ │ self = <Logger streamrip (DEBUG)> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /usr/lib/python3.13/logging/__init__.py:1737 in callHandlers │
│ │
│ 1734 │ │ │ for hdlr in c.handlers: │
│ 1735 │ │ │ │ found = found + 1 │
│ 1736 │ │ │ │ if record.levelno >= hdlr.level: │
│ ❱ 1737 │ │ │ │ │ hdlr.handle(record) │
│ 1738 │ │ │ if not c.propagate: │
│ 1739 │ │ │ │ c = None #break out │
│ 1740 │ │ │ else: │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ c = <RootLogger root (INFO)> │ │
│ │ found = 1 │ │
│ │ hdlr = <RichHandler (NOTSET)> │ │
│ │ record = <LogRecord: streamrip, 30, │ │
│ │ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/streamrip/… │ │
│ │ 358, "TIDAL: track not found"> │ │
│ │ self = <Logger streamrip (DEBUG)> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /usr/lib/python3.13/logging/__init__.py:1027 in handle │
│ │
│ 1024 │ │ │ record = rv │
│ 1025 │ │ if rv: │
│ 1026 │ │ │ with self.lock: │
│ ❱ 1027 │ │ │ │ self.emit(record) │
│ 1028 │ │ return rv │
│ 1029 │ │
│ 1030 │ def setFormatter(self, fmt): │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ record = <LogRecord: streamrip, 30, │ │
│ │ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/streamrip/… │ │
│ │ 358, "TIDAL: track not found"> │ │
│ │ rv = <LogRecord: streamrip, 30, │ │
│ │ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/streamrip/… │ │
│ │ 358, "TIDAL: track not found"> │ │
│ │ self = <RichHandler (NOTSET)> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/rich/logging.py:134 in │
│ emit │
│ │
│ 131 │ │
│ 132 │ def emit(self, record: LogRecord) -> None: │
│ 133 │ │ """Invoked by logging.""" │
│ ❱ 134 │ │ message = self.format(record) │
│ 135 │ │ traceback = None │
│ 136 │ │ if ( │
│ 137 │ │ │ self.rich_tracebacks │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ record = <LogRecord: streamrip, 30, │ │
│ │ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/streamrip/… │ │
│ │ 358, "TIDAL: track not found"> │ │
│ │ self = <RichHandler (NOTSET)> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /usr/lib/python3.13/logging/__init__.py:999 in format │
│ │
│ 996 │ │ │ fmt = self.formatter │
│ 997 │ │ else: │
│ 998 │ │ │ fmt = _defaultFormatter │
│ ❱ 999 │ │ return fmt.format(record) │
│ 1000 │ │
│ 1001 │ def emit(self, record): │
│ 1002 │ │ """ │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ fmt = <logging.Formatter object at 0x7f035615e0d0> │ │
│ │ record = <LogRecord: streamrip, 30, │ │
│ │ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/streamrip/… │ │
│ │ 358, "TIDAL: track not found"> │ │
│ │ self = <RichHandler (NOTSET)> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /usr/lib/python3.13/logging/__init__.py:712 in format │
│ │
│ 709 │ │ called to format the event time. If there is exception information, │
│ 710 │ │ it is formatted using formatException() and appended to the message. │
│ 711 │ │ """ │
│ ❱ 712 │ │ record.message = record.getMessage() │
│ 713 │ │ if self.usesTime(): │
│ 714 │ │ │ record.asctime = self.formatTime(record, self.datefmt) │
│ 715 │ │ s = self.formatMessage(record) │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ record = <LogRecord: streamrip, 30, │ │
│ │ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/streamrip/… │ │
│ │ 358, "TIDAL: track not found"> │ │
│ │ self = <logging.Formatter object at 0x7f035615e0d0> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /usr/lib/python3.13/logging/__init__.py:400 in getMessage │
│ │
│ 397 │ │ """ │
│ 398 │ │ msg = str(self.msg) │
│ 399 │ │ if self.args: │
│ ❱ 400 │ │ │ msg = msg % self.args │
│ 401 │ │ return msg │
│ 402 │
│ 403 # │
│ │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ msg = 'TIDAL: track not found' │ │
│ │ self = <LogRecord: streamrip, 30, │ │
│ │ /home/ivan/.local/share/uv/tools/streamrip/lib/python3.13/site-packages/streamrip/cl… │ │
│ │ 358, "TIDAL: track not found"> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
TypeError: not all arguments converted during string formatting
Config File
not needed
Operating System
Linux
streamrip version
rip, version 2.1.0
Screenshots and recordings
No response
Additional context
No response