import sys import time sys.path.insert(1, "..") import traceback import logging from typing import Optional, Union from aiohttp import ClientTimeout, ClientSession from lyric_search import utils from lyric_search.constructors import LyricsResult from . import common, cache, redis_cache from lyric_search.constructors import InvalidLRCLibResponseException logger = logging.getLogger() log_level = logging.getLevelName(logger.level) class LRCLib: """LRCLib Search Module""" def __init__(self) -> None: self.label: str = "LRCLib" self.lrclib_url: str = "https://lrclib.net/api/search" self.headers: dict = common.SCRAPE_HEADERS self.timeout = ClientTimeout(connect=2, sock_read=4) self.datautils = utils.DataUtils() self.matcher = utils.TrackMatcher() self.cache = cache.Cache() self.redis_cache = redis_cache.RedisCache() async def search( self, artist: str, song: str, plain: Optional[bool] = True ) -> Optional[LyricsResult]: """ LRCLib Search Args: artist (str): the artist to search song (str): the song to search Returns: Optional[LyricsResult]: The result, if found - None otherwise. """ try: artist: str = artist.strip().lower() song: str = song.strip().lower() time_start: float = time.time() lrc_obj: Optional[list[dict]] = None logging.info("Searching %s - %s on %s", artist, song, self.label) input_track: str = f"{artist} - {song}" returned_lyrics: str = "" async with ClientSession() as client: async with await client.get( self.lrclib_url, params={ "artist_name": artist, "track_name": song, }, timeout=self.timeout, headers=self.headers, ) as request: request.raise_for_status() text: Optional[str] = await request.text() if not text: raise InvalidLRCLibResponseException("No search response.") if len(text) < 100: raise InvalidLRCLibResponseException( "Search response text was invalid (len < 100 chars.)" ) search_data: Optional[Union[list, dict]] = await request.json() if not isinstance(search_data, list | dict): raise InvalidLRCLibResponseException("No JSON search data.") # logging.info("Search Data:\n%s", search_data) if not isinstance(search_data, list): raise InvalidLRCLibResponseException("Invalid JSON.") if plain: possible_matches = [ ( x, f"{result.get('artistName')} - {result.get('trackName')}", ) for x, result in enumerate(search_data) ] else: logging.info( "Limiting possible matches to only those with non-null syncedLyrics" ) possible_matches = [ ( x, f"{result.get('artistName')} - {result.get('trackName')}", ) for x, result in enumerate(search_data) if isinstance(result["syncedLyrics"], str) ] best_match = self.matcher.find_best_match( input_track, possible_matches )[0] if not best_match: return best_match_id = best_match[0] if not isinstance(search_data[best_match_id]["artistName"], str): raise InvalidLRCLibResponseException( f"Invalid JSON: Cannot find artistName key.\n{search_data}" ) if not isinstance(search_data[best_match_id]["trackName"], str): raise InvalidLRCLibResponseException( f"Invalid JSON: Cannot find trackName key.\n{search_data}" ) returned_artist: str = search_data[best_match_id]["artistName"] returned_song: str = search_data[best_match_id]["trackName"] if plain: if not isinstance( search_data[best_match_id]["plainLyrics"], str ): raise InvalidLRCLibResponseException( f"Invalid JSON: Cannot find plainLyrics key.\n{search_data}" ) returned_lyrics: str = search_data[best_match_id]["plainLyrics"] returned_lyrics = self.datautils.scrub_lyrics(returned_lyrics) else: if not isinstance( search_data[best_match_id]["syncedLyrics"], str ): raise InvalidLRCLibResponseException( f"Invalid JSON: Cannot find syncedLyrics key.\n{search_data}" ) returned_lyrics: str = search_data[best_match_id][ "syncedLyrics" ] lrc_obj = self.datautils.create_lrc_object(returned_lyrics) returned_track: str = f"{returned_artist} - {returned_song}" (_matched, confidence) = self.matcher.find_best_match( input_track=input_track, candidate_tracks=[(0, returned_track)] ) if not confidence: return # No suitable match found logging.info("Result found on %s", self.label) time_end: float = time.time() time_diff: float = time_end - time_start matched = LyricsResult( artist=returned_artist, song=returned_song, src=self.label, lyrics=returned_lyrics if plain else lrc_obj, confidence=confidence, time=time_diff, ) await self.redis_cache.increment_found_count(self.label) await self.cache.store(matched) return matched except: traceback.print_exc()