Added support for Tidal lyrics and fixed parsing error when preferred quality too high (#736)

* fixed json error when preferred quality not found for track

* added tidal lyrics support

* improve mp3 lyrics support

* handle error when there are no lyrics (404)

* change error to warning
This commit is contained in:
Mathieu Broillet 2024-12-07 07:41:47 +01:00 committed by GitHub
parent 1aad9f0f65
commit 4c9baf9bb0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 22 additions and 2 deletions

View file

@ -4,6 +4,7 @@ import json
import logging import logging
import re import re
import time import time
from json import JSONDecodeError
import aiohttp import aiohttp
@ -101,6 +102,17 @@ class TidalClient(Client):
item["albums"] = album_resp["items"] item["albums"] = album_resp["items"]
item["albums"].extend(ep_resp["items"]) item["albums"].extend(ep_resp["items"])
elif media_type == "track":
try:
resp = await self._api_request(f"tracks/{str(item_id)}/lyrics", base="https://listen.tidal.com/v1")
# Use unsynced lyrics for MP3, synced for others (FLAC, OPUS, etc)
if self.global_config.session.conversion.enabled and self.global_config.session.conversion.codec.upper() == "MP3":
item["lyrics"] = resp.get("lyrics") or ''
else:
item["lyrics"] = resp.get("subtitles") or resp.get("lyrics") or ''
except TypeError as e:
logger.warning(f"Failed to get lyrics for {item_id}: {e}")
logger.debug(item) logger.debug(item)
return item return item
@ -140,6 +152,9 @@ class TidalClient(Client):
manifest = json.loads(base64.b64decode(resp["manifest"]).decode("utf-8")) manifest = json.loads(base64.b64decode(resp["manifest"]).decode("utf-8"))
except KeyError: except KeyError:
raise Exception(resp["userMessage"]) raise Exception(resp["userMessage"])
except JSONDecodeError:
logger.warning(f"Failed to get manifest for {track_id}. Retrying with lower quality.")
return await self.get_downloadable(track_id, quality - 1)
logger.debug(manifest) logger.debug(manifest)
enc_key = manifest.get("keyId") enc_key = manifest.get("keyId")
@ -306,7 +321,7 @@ class TidalClient(Client):
async with self.session.post(url, data=data, auth=auth) as resp: async with self.session.post(url, data=data, auth=auth) as resp:
return await resp.json() return await resp.json()
async def _api_request(self, path: str, params=None) -> dict: async def _api_request(self, path: str, params=None, base: str = BASE) -> dict:
"""Handle Tidal API requests. """Handle Tidal API requests.
:param path: :param path:
@ -321,7 +336,7 @@ class TidalClient(Client):
params["limit"] = 100 params["limit"] = 100
async with self.rate_limiter: async with self.rate_limiter:
async with self.session.get(f"{BASE}/{path}", params=params) as resp: async with self.session.get(f"{base}/{path}", params=params) as resp:
if resp.status == 404: if resp.status == 404:
logger.warning("TIDAL: track not found", resp) logger.warning("TIDAL: track not found", resp)
raise NonStreamableError("TIDAL: Track not found") raise NonStreamableError("TIDAL: Track not found")

View file

@ -183,6 +183,7 @@ class Container(Enum):
"discnumber", "discnumber",
"composer", "composer",
"isrc", "isrc",
"lyrics",
} }
if attr in in_trackmetadata: if attr in in_trackmetadata:
if attr == "album": if attr == "album":

View file

@ -32,6 +32,7 @@ class TrackMetadata:
discnumber: int discnumber: int
composer: str | None composer: str | None
isrc: str | None = None isrc: str | None = None
lyrics: str | None = ""
@classmethod @classmethod
def from_qobuz(cls, album: AlbumMetadata, resp: dict) -> TrackMetadata | None: def from_qobuz(cls, album: AlbumMetadata, resp: dict) -> TrackMetadata | None:
@ -170,6 +171,8 @@ class TrackMetadata:
else: else:
artist = track["artist"]["name"] artist = track["artist"]["name"]
lyrics = track.get("lyrics", "")
quality_map: dict[str, int] = { quality_map: dict[str, int] = {
"LOW": 0, "LOW": 0,
"HIGH": 1, "HIGH": 1,
@ -209,6 +212,7 @@ class TrackMetadata:
discnumber=discnumber, discnumber=discnumber,
composer=None, composer=None,
isrc=isrc, isrc=isrc,
lyrics=lyrics
) )
@classmethod @classmethod