radio_util: open tracks SQLite DB in readonly mode; black: reformat files
This commit is contained in:
@ -9,23 +9,25 @@ from typing import LiteralString, Optional, Union, Iterable
|
||||
from regex import Pattern
|
||||
from .constructors import ValidTypeAheadRequest, ValidLyricRequest
|
||||
from lyric_search.constructors import LyricsResult
|
||||
from lyric_search.sources import aggregate
|
||||
from lyric_search.sources import aggregate
|
||||
from lyric_search import notifier
|
||||
|
||||
|
||||
class CacheUtils:
|
||||
"""
|
||||
Lyrics Cache DB Utils
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.lyrics_db_path: LiteralString = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs", "cached_lyrics.db")
|
||||
|
||||
self.lyrics_db_path: LiteralString = os.path.join(
|
||||
"/usr/local/share", "sqlite_dbs", "cached_lyrics.db"
|
||||
)
|
||||
|
||||
async def check_typeahead(self, query: str) -> Optional[list[str]]:
|
||||
"""Lyric Search Typeahead DB Handler"""
|
||||
if not query:
|
||||
return None
|
||||
async with sqlite3.connect(self.lyrics_db_path,
|
||||
timeout=1) as _db:
|
||||
async with sqlite3.connect(self.lyrics_db_path, timeout=1) as _db:
|
||||
_db.row_factory = sqlite3.Row
|
||||
db_query: str = """SELECT DISTINCT(LOWER(TRIM(artist) || " - " || TRIM(song))),\
|
||||
(TRIM(artist) || " - " || TRIM(song)) as ret FROM lyrics WHERE\
|
||||
@ -33,9 +35,7 @@ class CacheUtils:
|
||||
db_params: tuple[str] = (f"%%%{query}%%%",)
|
||||
async with _db.execute(db_query, db_params) as _cursor:
|
||||
result: Iterable[sqlite3.Row] = await _cursor.fetchall()
|
||||
out_result = [
|
||||
str(r['ret']) for r in result
|
||||
]
|
||||
out_result = [str(r["ret"]) for r in result]
|
||||
return out_result
|
||||
|
||||
|
||||
@ -43,18 +43,17 @@ class LyricSearch(FastAPI):
|
||||
"""
|
||||
Lyric Search Endpoint
|
||||
"""
|
||||
def __init__(self, app: FastAPI,
|
||||
util, constants) -> None:
|
||||
|
||||
def __init__(self, app: FastAPI, util, constants) -> None:
|
||||
self.app: FastAPI = app
|
||||
self.util = util
|
||||
self.constants = constants
|
||||
self.cache_utils = CacheUtils()
|
||||
self.notifier = notifier.DiscordNotifier()
|
||||
|
||||
|
||||
self.endpoints: dict = {
|
||||
"typeahead/lyrics": self.typeahead_handler,
|
||||
"lyric_search": self.lyric_search_handler, # Preserving old endpoint path temporarily
|
||||
"lyric_search": self.lyric_search_handler, # Preserving old endpoint path temporarily
|
||||
"lyric/search": self.lyric_search_handler,
|
||||
}
|
||||
|
||||
@ -66,11 +65,18 @@ class LyricSearch(FastAPI):
|
||||
"IRC-SHARED",
|
||||
]
|
||||
|
||||
self.lrc_regex: Pattern = regex.compile(r'\[([0-9]{2}:[0-9]{2})\.[0-9]{1,3}\](\s(.*)){0,}')
|
||||
self.lrc_regex: Pattern = regex.compile(
|
||||
r"\[([0-9]{2}:[0-9]{2})\.[0-9]{1,3}\](\s(.*)){0,}"
|
||||
)
|
||||
|
||||
for endpoint, handler in self.endpoints.items():
|
||||
_schema_include = endpoint in ["lyric/search"]
|
||||
app.add_api_route(f"/{endpoint}", handler, methods=["POST"], include_in_schema=_schema_include)
|
||||
app.add_api_route(
|
||||
f"/{endpoint}",
|
||||
handler,
|
||||
methods=["POST"],
|
||||
include_in_schema=_schema_include,
|
||||
)
|
||||
|
||||
async def typeahead_handler(self, data: ValidTypeAheadRequest) -> JSONResponse:
|
||||
"""
|
||||
@ -78,104 +84,133 @@ class LyricSearch(FastAPI):
|
||||
- **query**: Typeahead query
|
||||
"""
|
||||
if not isinstance(data.query, str):
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Invalid request.',
|
||||
})
|
||||
typeahead: Optional[list[str]] = await self.cache_utils.check_typeahead(data.query)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Invalid request.",
|
||||
},
|
||||
)
|
||||
typeahead: Optional[list[str]] = await self.cache_utils.check_typeahead(
|
||||
data.query
|
||||
)
|
||||
if not typeahead:
|
||||
return JSONResponse(content=[])
|
||||
return JSONResponse(content=typeahead)
|
||||
|
||||
|
||||
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]
|
||||
- **t**: track (artist and song combined) [used only if a & s are not used]
|
||||
- **extra**: include extra details in response [optional, default: false]
|
||||
- **lrc**: Request LRCs?
|
||||
- **lrc**: Request LRCs?
|
||||
- **sub**: text to search within lyrics, if found lyrics will begin at found verse [optional, default: none]
|
||||
- **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 JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': f'Unknown request source: {data.src}',
|
||||
})
|
||||
|
||||
await self.notifier.send(
|
||||
f"ERROR @ {__file__.rsplit("/", maxsplit=1)[-1]}",
|
||||
f"Unknown request source: {data.src}",
|
||||
)
|
||||
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: 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()))
|
||||
search_song = str(self.constants.DOUBLE_SPACE_REGEX.sub(" ", search_song.strip()))
|
||||
search_artist = str(
|
||||
self.constants.DOUBLE_SPACE_REGEX.sub(" ", search_artist.strip())
|
||||
)
|
||||
search_song = str(
|
||||
self.constants.DOUBLE_SPACE_REGEX.sub(" ", search_song.strip())
|
||||
)
|
||||
search_artist = urllib.parse.unquote(search_artist)
|
||||
search_song = urllib.parse.unquote(search_song)
|
||||
|
||||
if not isinstance(search_artist, str) or not isinstance(search_song, str):
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Invalid request',
|
||||
})
|
||||
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[Union[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 JSONResponse(content={
|
||||
'err': True,
|
||||
'errorText': 'Sources exhausted, lyrics not located.',
|
||||
})
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Sources exhausted, lyrics not located.",
|
||||
}
|
||||
)
|
||||
|
||||
result = vars(result)
|
||||
|
||||
|
||||
if data.sub and not data.lrc:
|
||||
seeked_found_line: Optional[int] = None
|
||||
lyric_lines: list[str] = result['lyrics'].strip().split(" / ")
|
||||
lyric_lines: list[str] = result["lyrics"].strip().split(" / ")
|
||||
for i, line in enumerate(lyric_lines):
|
||||
line = regex.sub(r'\u2064', '', line.strip())
|
||||
line = regex.sub(r"\u2064", "", line.strip())
|
||||
if data.sub.strip().lower() in line.strip().lower():
|
||||
seeked_found_line = i
|
||||
logging.debug("Found %s at %s, match for %s!",
|
||||
line, seeked_found_line, data.sub) # REMOVEME: DEBUG
|
||||
logging.debug(
|
||||
"Found %s at %s, match for %s!",
|
||||
line,
|
||||
seeked_found_line,
|
||||
data.sub,
|
||||
) # REMOVEME: DEBUG
|
||||
break
|
||||
|
||||
if not seeked_found_line:
|
||||
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['confidence'])
|
||||
result['time'] = f'{float(result['time']):.4f}'
|
||||
|
||||
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["confidence"])
|
||||
result["time"] = f"{float(result['time']):.4f}"
|
||||
|
||||
if plain_lyrics:
|
||||
result['lyrics'] = regex.sub(r'(\s/\s|\n)', '<br>', result['lyrics']).strip()
|
||||
result["lyrics"] = regex.sub(
|
||||
r"(\s/\s|\n)", "<br>", result["lyrics"]
|
||||
).strip()
|
||||
else:
|
||||
# Swap lyrics key for 'lrc'
|
||||
result['lrc'] = result['lyrics']
|
||||
result.pop('lyrics')
|
||||
result["lrc"] = result["lyrics"]
|
||||
result.pop("lyrics")
|
||||
|
||||
if "cache" in result['src']:
|
||||
result['from_cache'] = True
|
||||
if "cache" in result["src"]:
|
||||
result["from_cache"] = True
|
||||
|
||||
if not data.extra:
|
||||
result.pop('src')
|
||||
|
||||
return JSONResponse(content=result)
|
||||
result.pop("src")
|
||||
|
||||
return JSONResponse(content=result)
|
||||
|
Reference in New Issue
Block a user