misc
This commit is contained in:
@@ -2,7 +2,17 @@ from typing import Optional, Any
|
||||
from uuid import uuid4
|
||||
from urllib.parse import urlparse
|
||||
import hashlib
|
||||
import traceback
|
||||
import logging
|
||||
# Suppress all logging output from this module and its children
|
||||
for name in [__name__, "utils.sr_wrapper"]:
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(logging.CRITICAL)
|
||||
logger.propagate = False
|
||||
for handler in logger.handlers:
|
||||
handler.setLevel(logging.CRITICAL)
|
||||
# Also set the root logger to CRITICAL as a last resort (may affect global logging)
|
||||
logging.getLogger().setLevel(logging.CRITICAL)
|
||||
import random
|
||||
import asyncio
|
||||
import os
|
||||
@@ -11,6 +21,8 @@ import time
|
||||
from streamrip.client import TidalClient # type: ignore
|
||||
from streamrip.config import Config as StreamripConfig # type: ignore
|
||||
from dotenv import load_dotenv
|
||||
from rapidfuzz import fuzz
|
||||
|
||||
|
||||
|
||||
load_dotenv()
|
||||
@@ -62,7 +74,18 @@ class SRUtil:
|
||||
await asyncio.sleep(self.METADATA_RATE_LIMIT - elapsed)
|
||||
result = await func(*args, **kwargs)
|
||||
self.last_request_time = time.time()
|
||||
return result
|
||||
return result
|
||||
|
||||
def is_fuzzy_match(self, expected, actual, threshold=80):
|
||||
if not expected or not actual:
|
||||
return False
|
||||
return fuzz.token_set_ratio(expected.lower(), actual.lower()) >= threshold
|
||||
|
||||
def is_metadata_match(self, expected_artist, expected_album, expected_title, found_artist, found_album, found_title, threshold=80):
|
||||
artist_match = self.is_fuzzy_match(expected_artist, found_artist, threshold)
|
||||
album_match = self.is_fuzzy_match(expected_album, found_album, threshold) if expected_album else True
|
||||
title_match = self.is_fuzzy_match(expected_title, found_title, threshold)
|
||||
return artist_match and album_match and title_match
|
||||
|
||||
def dedupe_by_key(self, key: str, entries: list[dict]) -> list[dict]:
|
||||
deduped = {}
|
||||
@@ -77,6 +100,23 @@ class SRUtil:
|
||||
return None
|
||||
m, s = divmod(seconds, 60)
|
||||
return f"{m}:{s:02}"
|
||||
|
||||
def _get_tidal_cover_url(self, uuid, size):
|
||||
"""Generate a tidal cover url.
|
||||
|
||||
:param uuid: VALID uuid string
|
||||
:param size:
|
||||
"""
|
||||
TIDAL_COVER_URL = "https://resources.tidal.com/images/{uuid}/{width}x{height}.jpg"
|
||||
possibles = (80, 160, 320, 640, 1280)
|
||||
assert size in possibles, f"size must be in {possibles}"
|
||||
return TIDAL_COVER_URL.format(
|
||||
uuid=uuid.replace("-", "/"),
|
||||
height=size,
|
||||
width=size,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def combine_album_track_metadata(
|
||||
self, album_json: dict | None, track_json: dict
|
||||
@@ -140,32 +180,33 @@ class SRUtil:
|
||||
]
|
||||
|
||||
async def get_artists_by_name(self, artist_name: str) -> Optional[list]:
|
||||
"""Get artist(s) by name.
|
||||
Args:
|
||||
artist_name (str): The name of the artist.
|
||||
Returns:
|
||||
Optional[dict]: The artist details or None if not found.
|
||||
"""
|
||||
|
||||
try:
|
||||
await self.streamrip_client.login()
|
||||
except Exception as e:
|
||||
logging.info("Login Exception: %s", str(e))
|
||||
pass
|
||||
"""Get artist(s) by name. Retry login only on authentication failure. Rate limit and retry on 400/429."""
|
||||
import asyncio
|
||||
artists_out: list[dict] = []
|
||||
try:
|
||||
artists = await self.streamrip_client.search(
|
||||
media_type="artist", query=artist_name
|
||||
)
|
||||
except AttributeError:
|
||||
await self.streamrip_client.login()
|
||||
artists = await self.streamrip_client.search(
|
||||
media_type="artist", query=artist_name
|
||||
)
|
||||
logging.critical("Artists output: %s", artists)
|
||||
max_retries = 4
|
||||
delay = 1.0
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
artists = await self.streamrip_client.search(
|
||||
media_type="artist", query=artist_name
|
||||
)
|
||||
break
|
||||
except AttributeError:
|
||||
await self.streamrip_client.login()
|
||||
if attempt == max_retries - 1:
|
||||
return None
|
||||
except Exception as e:
|
||||
msg = str(e)
|
||||
if ("400" in msg or "429" in msg) and attempt < max_retries - 1:
|
||||
await asyncio.sleep(delay)
|
||||
delay *= 2
|
||||
continue
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
artists = artists[0].get("items", [])
|
||||
if not artists:
|
||||
logging.warning("No artist found for name: %s", artist_name)
|
||||
return None
|
||||
artists_out = [
|
||||
{
|
||||
@@ -179,26 +220,33 @@ class SRUtil:
|
||||
return artists_out
|
||||
|
||||
async def get_albums_by_artist_id(self, artist_id: int) -> Optional[list | dict]:
|
||||
"""Get albums by artist ID
|
||||
Args:
|
||||
artist_id (int): The ID of the artist.
|
||||
Returns:
|
||||
Optional[list[dict]]: List of albums or None if not found.
|
||||
"""
|
||||
"""Get albums by artist ID. Retry login only on authentication failure. Rate limit and retry on 400/429."""
|
||||
import asyncio
|
||||
artist_id_str: str = str(artist_id)
|
||||
albums_out: list[dict] = []
|
||||
try:
|
||||
await self.streamrip_client.login()
|
||||
metadata = await self.streamrip_client.get_metadata(
|
||||
item_id=artist_id_str, media_type="artist"
|
||||
)
|
||||
except AttributeError:
|
||||
await self.streamrip_client.login()
|
||||
metadata = await self.streamrip_client.get_metadata(
|
||||
item_id=artist_id_str, media_type="artist"
|
||||
)
|
||||
max_retries = 4
|
||||
delay = 1.0
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
metadata = await self.streamrip_client.get_metadata(
|
||||
item_id=artist_id_str, media_type="artist"
|
||||
)
|
||||
break
|
||||
except AttributeError:
|
||||
await self.streamrip_client.login()
|
||||
if attempt == max_retries - 1:
|
||||
return None
|
||||
except Exception as e:
|
||||
msg = str(e)
|
||||
if ("400" in msg or "429" in msg) and attempt < max_retries - 1:
|
||||
await asyncio.sleep(delay)
|
||||
delay *= 2
|
||||
continue
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
if not metadata:
|
||||
logging.warning("No metadata found for artist ID: %s", artist_id)
|
||||
return None
|
||||
albums = self.dedupe_by_key("title", metadata.get("albums", []))
|
||||
albums_out = [
|
||||
@@ -211,9 +259,65 @@ class SRUtil:
|
||||
for album in albums
|
||||
if "title" in album and "id" in album and "artists" in album
|
||||
]
|
||||
|
||||
logging.debug("Retrieved albums: %s", albums_out)
|
||||
return albums_out
|
||||
|
||||
async def get_album_by_name(self, artist: str, album: str) -> Optional[dict]:
|
||||
"""Get album by artist and album name using artist ID and fuzzy matching. Try first 8 chars, then 12 if no match. Notify on success."""
|
||||
# Notification moved to add_cover_art.py as requested
|
||||
for trunc in (8, 12):
|
||||
search_artist = artist[:trunc]
|
||||
artists = await self.get_artists_by_name(search_artist)
|
||||
if not artists:
|
||||
continue
|
||||
best_artist = None
|
||||
best_artist_score = 0
|
||||
for a in artists:
|
||||
score = fuzz.token_set_ratio(artist, a["artist"])
|
||||
if score > best_artist_score:
|
||||
best_artist = a
|
||||
best_artist_score = int(score)
|
||||
if not best_artist or best_artist_score < 85:
|
||||
continue
|
||||
artist_id = best_artist["id"]
|
||||
albums = await self.get_albums_by_artist_id(artist_id)
|
||||
if not albums:
|
||||
continue
|
||||
best_album = None
|
||||
best_album_score = 0
|
||||
for alb in albums:
|
||||
score = fuzz.token_set_ratio(album, alb["album"])
|
||||
if score > best_album_score:
|
||||
best_album = alb
|
||||
best_album_score = int(score)
|
||||
if best_album and best_album_score >= 85:
|
||||
return best_album
|
||||
return None
|
||||
|
||||
async def get_cover_by_album_id(self, album_id: int, size: int = 640) -> Optional[str]:
|
||||
"""Get cover URL by album ID. Retry login only on authentication failure."""
|
||||
if size not in [80, 160, 320, 640, 1280]:
|
||||
return None
|
||||
album_id_str: str = str(album_id)
|
||||
for attempt in range(2):
|
||||
try:
|
||||
metadata = await self.streamrip_client.get_metadata(
|
||||
item_id=album_id_str, media_type="album"
|
||||
)
|
||||
break
|
||||
except AttributeError:
|
||||
await self.streamrip_client.login()
|
||||
if attempt == 1:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
if not metadata:
|
||||
return None
|
||||
cover_id = metadata.get("cover")
|
||||
if not cover_id:
|
||||
return None
|
||||
cover_url = self._get_tidal_cover_url(cover_id, size)
|
||||
return cover_url
|
||||
|
||||
|
||||
async def get_tracks_by_album_id(
|
||||
self, album_id: int, quality: str = "FLAC"
|
||||
@@ -247,7 +351,7 @@ class SRUtil:
|
||||
]
|
||||
return tracks_out
|
||||
|
||||
async def get_tracks_by_artist_song(self, artist: str, song: str) -> Optional[list]:
|
||||
async def get_tracks_by_artist_song(self, artist: str, song: str, n: int = 0) -> Optional[list]:
|
||||
"""Get track by artist and song name
|
||||
Args:
|
||||
artist (str): The name of the artist.
|
||||
@@ -256,7 +360,23 @@ class SRUtil:
|
||||
Optional[dict]: The track details or None if not found.
|
||||
TODO: Reimplement using StreamRip
|
||||
"""
|
||||
return []
|
||||
if not self.streamrip_client.logged_in:
|
||||
await self.streamrip_client.login()
|
||||
try:
|
||||
search_res = await self.streamrip_client.search(media_type="track",
|
||||
query=f"{artist} - {song}",
|
||||
)
|
||||
logging.critical("Result: %s", search_res)
|
||||
return search_res[0].get('items')
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
logging.critical("Search Exception: %s", str(e))
|
||||
if n < 3:
|
||||
n+=1
|
||||
return await self.get_tracks_by_artist_song(artist, song, n)
|
||||
finally:
|
||||
return []
|
||||
# return []
|
||||
|
||||
async def get_stream_url_by_track_id(
|
||||
self, track_id: int, quality: str = "FLAC"
|
||||
|
Reference in New Issue
Block a user