This commit is contained in:
2025-02-15 21:09:33 -05:00
parent 60416c493f
commit 39d1ddaffa
22 changed files with 509 additions and 525 deletions

View File

@ -7,7 +7,8 @@ import urllib.parse
import regex
import aiosqlite as sqlite3
from fastapi import FastAPI, HTTPException
from typing import LiteralString, Optional, Callable
from fastapi.responses import JSONResponse
from typing import LiteralString, Optional, Union
from regex import Pattern
from .constructors import ValidTypeAheadRequest, ValidLyricRequest
from lyric_search.constructors import LyricsResult
@ -15,13 +16,18 @@ from lyric_search.sources import aggregate
from lyric_search import notifier
class CacheUtils:
"""Lyrics Cache DB Utils"""
"""
Lyrics Cache DB Utils
"""
def __init__(self) -> None:
self.lyrics_db_path: LiteralString = os.path.join("/usr/local/share",
"sqlite_dbs", "cached_lyrics.db")
async def check_typeahead(self, s: str, pre_query: str | None = None) -> list[dict]:
"""Check s against artists stored - for typeahead"""
async def check_typeahead(self, s: str,
pre_query: Optional[str] = None) -> list[dict]:
"""
Check s against artists stored - for typeahead
"""
async with sqlite3.connect(self.lyrics_db_path,
timeout=2) as db_conn:
db_conn.row_factory = sqlite3.Row
@ -36,9 +42,12 @@ class CacheUtils:
class LyricSearch(FastAPI):
"""Lyric Search Endpoint"""
def __init__(self, app: FastAPI, util, constants): # pylint: disable=super-init-not-called
self.app = app
"""
Lyric Search Endpoint
"""
def __init__(self, app: FastAPI,
util, constants) -> None: # pylint: disable=super-init-not-called
self.app: FastAPI = app
self.util = util
self.constants = constants
self.cache_utils = CacheUtils()
@ -70,36 +79,39 @@ class LyricSearch(FastAPI):
_schema_include = endpoint in ["lyric/search"]
app.add_api_route(f"/{endpoint}", handler, methods=["POST"], include_in_schema=_schema_include)
async def artist_typeahead_handler(self, data: ValidTypeAheadRequest) -> list[str]|dict:
"""Artist Type Ahead Handler"""
async def artist_typeahead_handler(self, data: ValidTypeAheadRequest) -> JSONResponse:
"""
Artist Type Ahead Handler
"""
if not isinstance(data.query, str) or len(data.query) < 2:
return {
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'Invalid request',
}
})
query: str = data.query
typeahead_result: list[dict] = await self.cache_utils.check_typeahead(query)
typeahead_list: list[str] = [str(r.get('artist')) for r in typeahead_result]
return typeahead_list
return JSONResponse(content=typeahead_list)
async def song_typeahead_handler(self, data: ValidTypeAheadRequest) -> list[str]|dict:
"""Song Type Ahead Handler"""
async def song_typeahead_handler(self, data: ValidTypeAheadRequest) -> JSONResponse:
"""
Song Type Ahead Handler
"""
if not isinstance(data.pre_query, str)\
or not isinstance(data.query, str|None):
return {
or not isinstance(data.query, str):
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'Invalid request',
}
})
pre_query: str = data.pre_query
query: str = data.query
typeahead_result: list[dict] = await self.cache_utils.check_typeahead(query, pre_query)
typeahead_list: list[str] = [str(r.get('song')) for r in typeahead_result]
return typeahead_list
return JSONResponse(content=typeahead_list)
async def lyric_search_handler(self, data: ValidLyricRequest) -> dict:
async def lyric_search_handler(self, data: ValidLyricRequest) -> JSONResponse:
"""
Search for lyrics
- **a**: artist
- **s**: song
- **t**: track (artist and song combined) [used only if a & s are not used]
@ -109,26 +121,23 @@ class LyricSearch(FastAPI):
- **src**: the script/utility which initiated the request
- **excluded_sources**: sources to exclude [optional, default: none]
"""
if (not data.a or not data.s) and not data.t or not data.src:
raise HTTPException(detail="Invalid request", status_code=500)
if data.src.upper() not in self.acceptable_request_sources:
await self.notifier.send(f"ERROR @ {__file__.rsplit("/", maxsplit=1)[-1]}",
f"Unknown request source: {data.src}")
return {
return JSONResponse(status_code=500, content={
'err': True,
'errorText': f'Unknown request source: {data.src}',
}
})
if not data.t:
search_artist: Optional[str] = data.a
search_song: Optional[str] = data.s
else:
t_split = data.t.split(" - ", maxsplit=1)
search_artist = t_split[0]
search_song = t_split[1]
t_split: tuple = tuple(data.t.split(" - ", maxsplit=1))
(search_artist, search_song) = t_split
if search_artist and search_song:
search_artist = str(self.constants.DOUBLE_SPACE_REGEX.sub(" ", search_artist.strip()))
@ -137,21 +146,21 @@ class LyricSearch(FastAPI):
search_song = urllib.parse.unquote(search_song)
if not isinstance(search_artist, str) or not isinstance(search_song, str):
return {
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'Invalid request',
}
})
excluded_sources: Optional[list] = data.excluded_sources
aggregate_search = aggregate.Aggregate(exclude_methods=excluded_sources)
plain_lyrics: bool = not data.lrc
result: Optional[LyricsResult|dict] = await aggregate_search.search(search_artist, search_song, plain_lyrics)
result: Optional[Union[LyricsResult, dict]] = await aggregate_search.search(search_artist, search_song, plain_lyrics)
if not result:
return {
return JSONResponse(content={
'err': True,
'errorText': 'Sources exhausted, lyrics not located.',
}
})
result = vars(result)
@ -167,9 +176,11 @@ class LyricSearch(FastAPI):
break
if not seeked_found_line:
return {
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'Seek (a.k.a. subsearch) failed.',
'failed_seek': True,
}
})
result['lyrics'] = " / ".join(lyric_lines[seeked_found_line:])
result['confidence'] = int(result.get('confidence', 0))
@ -188,4 +199,4 @@ class LyricSearch(FastAPI):
if not data.extra:
result.pop('src')
return result
return JSONResponse(content=result)