significant refactor/cleanup

This commit is contained in:
2025-02-11 20:01:07 -05:00
parent 2c368aaf1a
commit 88d870ce8f
12 changed files with 440 additions and 467 deletions

View File

@ -13,18 +13,12 @@ from . import radio_util
from .constructors import ValidRadioNextRequest, ValidRadioReshuffleRequest, ValidRadioQueueShiftRequest,\
ValidRadioQueueRemovalRequest, ValidRadioSongRequest, RadioException
from uuid import uuid4 as uuid
from typing import Optional
from typing import Optional, LiteralString
from fastapi import FastAPI, BackgroundTasks, Request, Response, HTTPException
from fastapi.responses import RedirectResponse
from aiohttp import ClientSession, ClientTimeout
# pylint: disable=bare-except, broad-exception-caught, invalid-name
double_space = regex.compile(r'\s{2,}')
"""
TODO:
minor refactoring/type annotations/docstrings
"""
class Radio(FastAPI):
"""Radio Endpoints"""
@ -34,24 +28,8 @@ class Radio(FastAPI):
self.constants = constants
self.radio_util = radio_util.RadioUtil(self.constants)
self.glob_state = glob_state
self.ls_uri = "http://10.10.10.101:29000"
self.sqlite_exts: list[str] = ['/home/singer/api/solibs/spellfix1.cpython-311-x86_64-linux-gnu.so']
self.active_playlist_path = os.path.join("/usr/local/share",
"sqlite_dbs", "track_file_map.db")
self.active_playlist_name = "default" # not used
self.active_playlist = []
self.now_playing = {
'artist': 'N/A',
'song': 'N/A',
'genre': 'N/A',
'artistsong': 'N/A - N/A',
'duration': 0,
'start': 0,
'end': 0,
'file_path': None,
'id': None,
}
self.endpoints = {
self.endpoints: dict = {
"radio/np": self.radio_now_playing,
"radio/request": self.radio_request,
"radio/get_queue": self.radio_get_queue,
@ -69,30 +47,8 @@ class Radio(FastAPI):
# NOTE: Not in loop because method is GET for this endpoint
app.add_api_route("/radio/album_art", self.album_art_handler, methods=["GET"],
include_in_schema=True)
asyncio.get_event_loop().run_until_complete(self.load_playlist())
asyncio.get_event_loop().run_until_complete(self._ls_skip())
def get_queue_item_by_uuid(self, uuid: str) -> tuple[int, dict] | None:
"""
Get queue item by UUID
Args:
uuid: The UUID to search
Returns:
dict|None
"""
for x, item in enumerate(self.active_playlist):
if item.get('uuid') == uuid:
return (x, item)
return None
async def _ls_skip(self) -> bool:
async with ClientSession() as session:
async with session.get(f"{self.ls_uri}/next",
timeout=ClientTimeout(connect=2, sock_read=2)) as request:
request.raise_for_status()
text = await request.text()
return text == "OK"
asyncio.get_event_loop().run_until_complete(self.radio_util.load_playlist())
asyncio.get_event_loop().run_until_complete(self.radio_util._ls_skip())
async def radio_skip(self, data: ValidRadioNextRequest, request: Request) -> bool:
"""
@ -102,11 +58,11 @@ class Radio(FastAPI):
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
if data.skipTo:
(x, _) = self.get_queue_item_by_uuid(data.skipTo)
self.active_playlist = self.active_playlist[x:]
if not self.active_playlist:
await self.load_playlist()
return await self._ls_skip()
(x, _) = self.radio_util.get_queue_item_by_uuid(data.skipTo)
self.radio_util.active_playlist: list = self.radio_util.active_playlist[x:]
if not self.radio_util.active_playlist:
await self.radio_util.load_playlist()
return await self.radio_util._ls_skip()
except Exception as e:
traceback.print_exc()
return False
@ -119,7 +75,7 @@ class Radio(FastAPI):
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
random.shuffle(self.active_playlist)
random.shuffle(self.radio_util.active_playlist)
return {
'ok': True
}
@ -128,13 +84,9 @@ class Radio(FastAPI):
async def radio_get_queue(self, request: Request, limit: int = 20_000) -> dict:
"""
Get current play queue, up to limit n [default: 20k]
Args:
limit (int): Number of results to return (default 20k)
Returns:
dict
"""
queue_out = []
for x, item in enumerate(self.active_playlist[0:limit+1]):
for x, item in enumerate(self.radio_util.active_playlist[0:limit+1]):
queue_out.append({
'pos': x,
'id': item.get('id'),
@ -153,11 +105,11 @@ class Radio(FastAPI):
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
(x, item) = self.get_queue_item_by_uuid(data.uuid)
self.active_playlist.pop(x)
self.active_playlist.insert(0, item)
(x, item) = self.radio_util.get_queue_item_by_uuid(data.uuid)
self.radio_util.active_playlist.pop(x)
self.radio_util.active_playlist.insert(0, item)
if not data.next:
await self._ls_skip()
await self.radio_util._ls_skip()
return {
'ok': True,
}
@ -167,158 +119,25 @@ class Radio(FastAPI):
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
(x, found_item) = self.get_queue_item_by_uuid(data.uuid)
(x, found_item) = self.radio_util.get_queue_item_by_uuid(data.uuid)
if not found_item:
return {
'ok': False,
'err': 'UUID not found in play queue',
}
self.active_playlist.pop(x)
self.radio_util.active_playlist.pop(x)
return {
'ok': True,
}
async def search_playlist(self, artistsong: str|None = None, artist: str|None = None, song: str|None = None) -> bool:
if artistsong and (artist or song):
raise RadioException("Cannot search using combination provided")
if not artistsong and (not artist or not song):
raise RadioException("No query provided")
try:
search_artist = None
search_song = None
search_query = 'SELECT id, artist, song, (artist || " - " || song) AS artistsong, genre, file_path, duration FROM tracks\
WHERE editdist3((lower(artist) || " " || lower(song)), (? || " " || ?))\
<= 410 ORDER BY editdist3((lower(artist) || " " || lower(song)), ?) ASC LIMIT 1'
if artistsong:
artistsong_split = artistsong.split(" - ", maxsplit=1)
(search_artist, search_song) = tuple(artistsong_split)
else:
search_artist = artist
search_song = song
if not artistsong:
artistsong = f"{search_artist} - {search_song}"
search_params = (search_artist.lower(), search_song.lower(), artistsong.lower(),)
async with sqlite3.connect(self.active_playlist_path,
timeout=2) as db_conn:
await db_conn.enable_load_extension(True)
for ext in self.sqlite_exts:
await db_conn.load_extension(ext)
db_conn.row_factory = sqlite3.Row
async with await db_conn.execute(search_query, search_params) as db_cursor:
result = await db_cursor.fetchone()
if not result:
return False
pushObj = {
'id': result['id'],
'uuid': str(uuid().hex),
'artist': result['artist'].strip(),
'song': result['song'].strip(),
'artistsong': result['artistsong'].strip(),
'genre': result['genre'],
'file_path': result['file_path'],
'duration': result['duration'],
}
self.active_playlist.insert(0, pushObj)
return True
except Exception as e:
logging.critical("search_playlist:: Search error occurred: %s", str(e))
traceback.print_exc()
return False
async def load_playlist(self):
try:
logging.info(f"Loading playlist...")
self.active_playlist.clear()
# db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, genre, file_path, duration FROM tracks\
# GROUP BY artistdashsong ORDER BY RANDOM()'
"""
LIMITED GENRES
"""
db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, genre, file_path, duration FROM tracks\
WHERE genre IN ("metalcore", "pop punk", "punk rock", "metal", "punk", "electronic", "nu metal", "EDM",\
"post-hardcore", "pop rock", "experimental", "post-punk", "death metal", "electronicore", "hard rock", "psychedelic rock",\
"grunge", "house", "dubstep", "hardcore", "hair metal", "horror punk", "folk punk", "breakcore",\
"post-rock", "deathcore", "hardcore punk", "synthwave", "trap") GROUP BY artistdashsong ORDER BY RANDOM()'
async with sqlite3.connect(self.active_playlist_path,
timeout=2) as db_conn:
db_conn.row_factory = sqlite3.Row
async with await db_conn.execute(db_query) as db_cursor:
results = await db_cursor.fetchall()
self.active_playlist = [{
'uuid': str(uuid().hex),
'id': r['id'],
'artist': double_space.sub(' ', r['artist']).strip(),
'song': double_space.sub(' ', r['song']).strip(),
'genre': r['genre'] if r['genre'] else 'Unknown',
'artistsong': double_space.sub(' ', r['artistdashsong']).strip(),
'file_path': r['file_path'],
'duration': r['duration'],
} for r in results]
logging.info("Populated active playlists with %s items",
len(self.active_playlist))
except:
traceback.print_exc()
async def cache_album_art(self, track_id: int, album_art: bytes) -> None:
try:
async with sqlite3.connect(self.active_playlist_path,
timeout=2) as db_conn:
async with await db_conn.execute("UPDATE tracks SET album_art = ? WHERE id = ?",
(album_art, track_id,)) as db_cursor:
await db_conn.commit()
except:
traceback.print_exc()
async def get_album_art(self, track_id: Optional[int] = None,
file_path: Optional[str] = None) -> bytes:
try:
async with sqlite3.connect(self.active_playlist_path,
timeout=2) as db_conn:
db_conn.row_factory = sqlite3.Row
query = "SELECT album_art FROM tracks WHERE id = ?"
query_params = (track_id,)
if file_path and not track_id:
query = "SELECT album_art FROM tracks WHERE file_path = ?"
query_params = (file_path,)
async with await db_conn.execute(query,
query_params) as db_cursor:
result = await db_cursor.fetchone()
if not result:
return
return result['album_art']
except:
traceback.print_exc()
return
async def _get_album_art(self, track_id: Optional[int] = None, file_path: Optional[str] = None) -> bytes|None:
try:
if not file_path:
file_path = self.now_playing.get('file_path')
if not file_path:
logging.critical("_get_album_art:: No current file")
return
original_file_path = file_path
file_path = file_path.replace("/paul/toons/",
"/singer/gogs_toons/")
cached_album_art = await self.get_album_art(file_path=original_file_path,
track_id=track_id)
if cached_album_art:
return cached_album_art
except:
traceback.print_exc()
# TODO: Optimize/cache
async def album_art_handler(self, request: Request, track_id: Optional[int] = None) -> bytes:
"""
Get album art, optional parameter track_id may be specified.
Otherwise, current track album art will be pulled.
"""
try:
logging.debug("Seeking album art with trackId: %s", track_id)
album_art = await self._get_album_art(track_id=track_id)
album_art: Optional[bytes] = await self.radio_util._get_album_art(track_id=track_id)
if not album_art:
return RedirectResponse(url="https://codey.lol/images/radio_art_default.jpg",
status_code=302)
@ -330,16 +149,14 @@ class Radio(FastAPI):
status_code=302)
async def radio_now_playing(self, request: Request) -> dict:
ret_obj = {**self.now_playing}
cur_elapsed = self.now_playing.get('elapsed', -1)
cur_duration = self.now_playing.get('duration', 999999)
ret_obj: dict = {**self.radio_util.now_playing}
cur_elapsed: int = self.radio_util.now_playing.get('elapsed', -1)
cur_duration: int = self.radio_util.now_playing.get('duration', 999999)
try:
ret_obj['elapsed'] = int(time.time()) - ret_obj['start']
except KeyError:
traceback.print_exc()
ret_obj['elapsed'] = 0
elapsed = ret_obj['elapsed']
duration = ret_obj['duration']
ret_obj.pop('file_path')
return ret_obj
@ -348,36 +165,31 @@ class Radio(FastAPI):
background_tasks: BackgroundTasks) -> Optional[dict]:
"""
Get next track
Args:
None
Returns:
str: Next track in queue
Track will be removed from the queue in the process (pop from top of list).
Track will be removed from the queue in the process.
"""
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
if not isinstance(self.active_playlist, list) or not self.active_playlist:
await self.load_playlist()
await self._ls_skip()
if not isinstance(self.radio_util.active_playlist, list) or not self.radio_util.active_playlist:
await self.radio_util.load_playlist()
await self.radio_util._ls_skip()
return
next = self.active_playlist.pop(0)
next = self.radio_util.active_playlist.pop(0)
if not isinstance(next, dict):
logging.critical("next is of type: %s, reloading playlist...", type(next))
await self.load_playlist()
await self._ls_skip()
await self.radio_util.load_playlist()
await self.radio_util._ls_skip()
return
duration = next['duration']
time_started = int(time.time())
time_ends = int(time_started + duration)
duration: int = next['duration']
time_started: int = int(time.time())
time_ends: int = int(time_started + duration)
if len(self.active_playlist) > 1:
self.active_playlist.append(next) # Push to end of playlist
if len(self.radio_util.active_playlist) > 1:
self.radio_util.active_playlist.append(next) # Push to end of playlist
else:
await self.load_playlist()
await self.radio_util.load_playlist()
self.now_playing = next
self.radio_util.now_playing: dict = next
next['start'] = time_started
next['end'] = time_ends
try:
@ -385,9 +197,9 @@ class Radio(FastAPI):
except Exception as e:
traceback.print_exc()
try:
if not await self.get_album_art(file_path=next['file_path']):
album_art = await self._get_album_art(next['file_path'])
await self.cache_album_art(next['id'], album_art)
if not await self.radio_util.get_album_art(file_path=next['file_path']):
album_art = await self.radio_util._get_album_art(next['file_path'])
await self.radio_util.cache_album_art(next['id'], album_art)
except:
traceback.print_exc()
return next
@ -396,9 +208,9 @@ class Radio(FastAPI):
async def radio_request(self, data: ValidRadioSongRequest, request: Request) -> Response:
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
artistsong = data.artistsong
artist = data.artist
song = data.song
artistsong: str = data.artistsong
artist: str = data.artist
song: str = data.song
if artistsong and (artist or song):
return {
'err': True,
@ -410,9 +222,11 @@ class Radio(FastAPI):
'errorText': 'Invalid request',
}
search = await self.search_playlist(artistsong=artistsong,
search: bool = await self.radio_util.search_playlist(artistsong=artistsong,
artist=artist,
song=song)
if data.alsoSkip:
await self._ls_skip()
return {'result': search}
await self.radio_util._ls_skip()
return {
'result': search
}