radio_util: open tracks SQLite DB in readonly mode; black: reformat files

This commit is contained in:
2025-04-17 07:28:05 -04:00
parent 96add377df
commit 6c88c23a4d
25 changed files with 1913 additions and 1340 deletions

View File

@ -4,21 +4,26 @@ import time
import random
import asyncio
from utils import radio_util
from .constructors import (ValidRadioNextRequest, ValidRadioReshuffleRequest,
ValidRadioQueueShiftRequest, ValidRadioQueueRemovalRequest,
ValidRadioSongRequest, ValidRadioTypeaheadRequest,
RadioException)
from .constructors import (
ValidRadioNextRequest,
ValidRadioReshuffleRequest,
ValidRadioQueueShiftRequest,
ValidRadioQueueRemovalRequest,
ValidRadioSongRequest,
ValidRadioTypeaheadRequest,
RadioException,
)
from uuid import uuid4 as uuid
from typing import Optional
from fastapi import (FastAPI, BackgroundTasks, Request,
Response, HTTPException)
from fastapi import FastAPI, BackgroundTasks, Request, Response, HTTPException
from fastapi.responses import RedirectResponse, JSONResponse
class Radio(FastAPI):
"""Radio Endpoints"""
def __init__(self, app: FastAPI,
my_util, constants) -> None:
def __init__(self, app: FastAPI, my_util, constants) -> None:
self.app: FastAPI = app
self.util = my_util
self.constants = constants
@ -33,22 +38,28 @@ class Radio(FastAPI):
"radio/queue_shift": self.radio_queue_shift,
"radio/reshuffle": self.radio_reshuffle,
"radio/queue_remove": self.radio_queue_remove,
"radio/ls._next_": self.radio_get_next,
"radio/ls._next_": self.radio_get_next,
}
for endpoint, handler in self.endpoints.items():
app.add_api_route(f"/{endpoint}", handler, methods=["POST"],
include_in_schema=True)
# 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)
app.add_api_route(
f"/{endpoint}", handler, methods=["POST"], include_in_schema=True
)
# 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.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) -> JSONResponse:
asyncio.get_event_loop().run_until_complete(self.radio_util._ls_skip())
async def radio_skip(
self, data: ValidRadioNextRequest, request: Request
) -> JSONResponse:
"""
Skip to the next track in the queue, or to uuid specified in skipTo if provided
- **key**: API key
@ -58,45 +69,54 @@ 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:
queue_item = self.radio_util.get_queue_item_by_uuid(data.skipTo)
queue_item = self.radio_util.get_queue_item_by_uuid(data.skipTo)
if not queue_item:
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'No such queue item.',
})
self.radio_util.active_playlist = self.radio_util.active_playlist[queue_item[0]:]
return JSONResponse(
status_code=500,
content={
"err": True,
"errorText": "No such queue item.",
},
)
self.radio_util.active_playlist = self.radio_util.active_playlist[
queue_item[0] :
]
if not self.radio_util.active_playlist:
await self.radio_util.load_playlist()
skip_result: bool = await self.radio_util._ls_skip()
status_code = 200 if skip_result else 500
return JSONResponse(status_code=status_code, content={
'success': skip_result,
})
return JSONResponse(
status_code=status_code,
content={
"success": skip_result,
},
)
except Exception as e:
traceback.print_exc()
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'General failure.',
})
async def radio_reshuffle(self, data: ValidRadioReshuffleRequest,
request: Request) -> JSONResponse:
return JSONResponse(
status_code=500,
content={
"err": True,
"errorText": "General failure.",
},
)
async def radio_reshuffle(
self, data: ValidRadioReshuffleRequest, request: Request
) -> JSONResponse:
"""
Reshuffle the play queue
- **key**: API key
"""
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.radio_util.active_playlist)
return JSONResponse(content={
'ok': True
})
async def radio_get_queue(self, request: Request,
limit: Optional[int] = 15_000) -> JSONResponse:
return JSONResponse(content={"ok": True})
async def radio_get_queue(
self, request: Request, limit: Optional[int] = 15_000
) -> JSONResponse:
"""
Get current play queue, up to limit [default: 15k]
- **limit**: Number of queue items to return, default 15k
@ -104,23 +124,24 @@ class Radio(FastAPI):
queue: list = self.radio_util.active_playlist[0:limit]
queue_out: list[dict] = []
for x, item in enumerate(queue):
queue_out.append({
'pos': x,
'id': item.get('id'),
'uuid': item.get('uuid'),
'artist': item.get('artist'),
'song': item.get('song'),
'album': item.get('album', 'N/A'),
'genre': item.get('genre', 'N/A'),
'artistsong': item.get('artistsong'),
'duration': item.get('duration'),
})
return JSONResponse(content={
'items': queue_out
})
async def radio_queue_shift(self, data: ValidRadioQueueShiftRequest,
request: Request) -> JSONResponse:
queue_out.append(
{
"pos": x,
"id": item.get("id"),
"uuid": item.get("uuid"),
"artist": item.get("artist"),
"song": item.get("song"),
"album": item.get("album", "N/A"),
"genre": item.get("genre", "N/A"),
"artistsong": item.get("artistsong"),
"duration": item.get("duration"),
}
)
return JSONResponse(content={"items": queue_out})
async def radio_queue_shift(
self, data: ValidRadioQueueShiftRequest, request: Request
) -> JSONResponse:
"""
Shift position of a UUID within the queue
[currently limited to playing next or immediately]
@ -130,24 +151,30 @@ 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")
queue_item = self.radio_util.get_queue_item_by_uuid(data.uuid)
if not queue_item:
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'Queue item not found.',
})
return JSONResponse(
status_code=500,
content={
"err": True,
"errorText": "Queue item not found.",
},
)
(x, item) = queue_item
self.radio_util.active_playlist.pop(x)
self.radio_util.active_playlist.pop(x)
self.radio_util.active_playlist.insert(0, item)
if not data.next:
await self.radio_util._ls_skip()
return JSONResponse(content={
'ok': True,
})
async def radio_queue_remove(self, data: ValidRadioQueueRemovalRequest,
request: Request) -> JSONResponse:
return JSONResponse(
content={
"ok": True,
}
)
async def radio_queue_remove(
self, data: ValidRadioQueueRemovalRequest, request: Request
) -> JSONResponse:
"""
Remove an item from the current play queue
- **key**: API key
@ -155,19 +182,26 @@ 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")
queue_item = self.radio_util.get_queue_item_by_uuid(data.uuid)
if not queue_item:
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'Queue item not found.',
})
return JSONResponse(
status_code=500,
content={
"err": True,
"errorText": "Queue item not found.",
},
)
self.radio_util.active_playlist.pop(queue_item[0])
return JSONResponse(content={
'ok': True,
})
async def album_art_handler(self, request: Request, track_id: Optional[int] = None) -> Response:
return JSONResponse(
content={
"ok": True,
}
)
async def album_art_handler(
self, request: Request, track_id: Optional[int] = None
) -> Response:
"""
Get album art, optional parameter track_id may be specified.
Otherwise, current track album art will be pulled.
@ -175,35 +209,42 @@ class Radio(FastAPI):
"""
try:
if not track_id:
track_id = self.radio_util.now_playing.get('id')
track_id = self.radio_util.now_playing.get("id")
logging.debug("Seeking album art with trackId: %s", track_id)
album_art: Optional[bytes] = await self.radio_util.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)
return Response(content=album_art,
media_type="image/png")
return RedirectResponse(
url="https://codey.lol/images/radio_art_default.jpg",
status_code=302,
)
return Response(content=album_art, media_type="image/png")
except Exception as e:
traceback.print_exc()
return RedirectResponse(url="https://codey.lol/images/radio_art_default.jpg",
status_code=302)
return RedirectResponse(
url="https://codey.lol/images/radio_art_default.jpg", status_code=302
)
async def radio_now_playing(self, request: Request) -> JSONResponse:
"""
Get currently playing track info
"""
ret_obj: dict = {**self.radio_util.now_playing}
try:
ret_obj['elapsed'] = int(time.time()) - ret_obj['start']
ret_obj["elapsed"] = int(time.time()) - ret_obj["start"]
except KeyError:
traceback.print_exc()
ret_obj['elapsed'] = 0
ret_obj.pop('file_path')
ret_obj["elapsed"] = 0
ret_obj.pop("file_path")
return JSONResponse(content=ret_obj)
async def radio_get_next(self, data: ValidRadioNextRequest, request: Request,
background_tasks: BackgroundTasks) -> JSONResponse:
async def radio_get_next(
self,
data: ValidRadioNextRequest,
request: Request,
background_tasks: BackgroundTasks,
) -> JSONResponse:
"""
Get next track
Track will be removed from the queue in the process.
@ -211,54 +252,65 @@ class Radio(FastAPI):
- **skipTo**: Optional UUID to skip to
"""
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.radio_util.active_playlist, list) or not self.radio_util.active_playlist:
raise HTTPException(status_code=403, detail="Unauthorized")
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 JSONResponse(status_code=500, content={
'err': True,
'errorText': 'General failure occurred, prompting playlist reload.',
})
return JSONResponse(
status_code=500,
content={
"err": True,
"errorText": "General failure occurred, prompting playlist reload.",
},
)
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.radio_util.load_playlist()
await self.radio_util._ls_skip()
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'General failure occurred, prompting playlist reload.',
})
duration: int = next['duration']
return JSONResponse(
status_code=500,
content={
"err": True,
"errorText": "General failure occurred, prompting playlist reload.",
},
)
duration: int = next["duration"]
time_started: int = int(time.time())
time_ends: int = int(time_started + duration)
time_ends: int = int(time_started + duration)
if len(self.radio_util.active_playlist) > 1:
self.radio_util.active_playlist.append(next) # Push to end of playlist
self.radio_util.active_playlist.append(next) # Push to end of playlist
else:
await self.radio_util.load_playlist()
self.radio_util.now_playing = next
next['start'] = time_started
next['end'] = time_ends
next["start"] = time_started
next["end"] = time_ends
try:
background_tasks.add_task(self.radio_util.webhook_song_change, next)
except Exception as e:
traceback.print_exc()
try:
if not await self.radio_util.get_album_art(file_path=next['file_path']):
album_art = await self.radio_util.get_album_art(file_path=next['file_path'])
if not await self.radio_util.get_album_art(file_path=next["file_path"]):
album_art = await self.radio_util.get_album_art(
file_path=next["file_path"]
)
if album_art:
await self.radio_util.cache_album_art(next['id'], album_art)
await self.radio_util.cache_album_art(next["id"], album_art)
else:
logging.debug("Could not read album art for %s",
next['file_path'])
logging.debug("Could not read album art for %s", next["file_path"])
except:
traceback.print_exc()
return JSONResponse(content=next)
async def radio_request(self, data: ValidRadioSongRequest, request: Request) -> JSONResponse:
async def radio_request(
self, data: ValidRadioSongRequest, request: Request
) -> JSONResponse:
"""
Song request handler
- **key**: API key
@ -268,42 +320,52 @@ class Radio(FastAPI):
- **alsoSkip**: If True, skips to the track; otherwise, track will be placed next up in queue
"""
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
raise HTTPException(status_code=403, detail="Unauthorized")
artistsong: Optional[str] = data.artistsong
artist: Optional[str] = data.artist
song: Optional[str] = data.song
if artistsong and (artist or song):
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'Invalid request',
})
return JSONResponse(
status_code=500,
content={
"err": True,
"errorText": "Invalid request",
},
)
if not artistsong and (not artist or not song):
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'Invalid request',
})
search: bool = await self.radio_util.search_playlist(artistsong=artistsong,
artist=artist,
song=song)
return JSONResponse(
status_code=500,
content={
"err": True,
"errorText": "Invalid request",
},
)
search: bool = await self.radio_util.search_playlist(
artistsong=artistsong, artist=artist, song=song
)
if data.alsoSkip:
await self.radio_util._ls_skip()
return JSONResponse(content={
'result': search
})
async def radio_typeahead(self, data: ValidRadioTypeaheadRequest,
request: Request) -> JSONResponse:
return JSONResponse(content={"result": search})
async def radio_typeahead(
self, data: ValidRadioTypeaheadRequest, request: Request
) -> JSONResponse:
"""
Radio typeahead handler
- **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.radio_util.trackdb_typeahead(data.query)
return JSONResponse(
status_code=500,
content={
"err": True,
"errorText": "Invalid request.",
},
)
typeahead: Optional[list[str]] = await self.radio_util.trackdb_typeahead(
data.query
)
if not typeahead:
return JSONResponse(content=[])
return JSONResponse(content=typeahead)
return JSONResponse(content=typeahead)