api/endpoints/radio.py

308 lines
13 KiB
Python
Raw Normal View History

import logging
import traceback
import time
2025-02-10 20:29:57 -05:00
import random
import asyncio
2025-02-10 20:29:57 -05:00
from . import radio_util
2025-02-11 11:19:52 -05:00
from .constructors import ValidRadioNextRequest, ValidRadioReshuffleRequest, ValidRadioQueueShiftRequest,\
2025-02-16 13:54:28 -05:00
ValidRadioQueueRemovalRequest, ValidRadioSongRequest,\
ValidRadioTypeaheadRequest, RadioException
from uuid import uuid4 as uuid
2025-02-15 21:09:33 -05:00
from typing import Optional
2025-02-10 20:29:57 -05:00
from fastapi import FastAPI, BackgroundTasks, Request, Response, HTTPException
2025-02-15 21:09:33 -05:00
from fastapi.responses import RedirectResponse, JSONResponse
2025-02-11 20:49:14 -05:00
"""
TODO:
- Radio request typeahead
"""
2025-02-11 11:26:20 -05:00
class Radio(FastAPI):
"""Radio Endpoints"""
2025-02-15 21:09:33 -05:00
def __init__(self, app: FastAPI,
2025-02-16 08:50:53 -05:00
my_util, constants) -> None:
2025-02-15 21:09:33 -05:00
self.app: FastAPI = app
self.util = my_util
self.constants = constants
2025-02-10 20:29:57 -05:00
self.radio_util = radio_util.RadioUtil(self.constants)
2025-02-11 20:01:07 -05:00
self.endpoints: dict = {
"radio/np": self.radio_now_playing,
"radio/request": self.radio_request,
2025-02-16 13:54:28 -05:00
"radio/typeahead": self.radio_typeahead,
"radio/get_queue": self.radio_get_queue,
"radio/skip": self.radio_skip,
2025-02-10 20:29:57 -05:00
"radio/queue_shift": self.radio_queue_shift,
"radio/reshuffle": self.radio_reshuffle,
"radio/queue_remove": self.radio_queue_remove,
2025-02-11 08:41:29 -05:00
"radio/ls._next_": self.radio_get_next,
}
for endpoint, handler in self.endpoints.items():
app.add_api_route(f"/{endpoint}", handler, methods=["POST"],
2025-02-16 07:56:33 -05:00
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"],
2025-02-16 07:56:33 -05:00
include_in_schema=True)
2025-02-11 20:01:07 -05:00
asyncio.get_event_loop().run_until_complete(self.radio_util.load_playlist())
asyncio.get_event_loop().run_until_complete(self.radio_util._ls_skip())
2025-02-15 21:09:33 -05:00
async def radio_skip(self, data: ValidRadioNextRequest,
request: Request) -> JSONResponse:
"""
2025-02-10 20:29:57 -05:00
Skip to the next track in the queue, or to uuid specified in skipTo if provided
2025-02-16 08:17:27 -05:00
- **key**: API key
- **skipTo**: Optional UUID to skip to
"""
try:
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
2025-02-10 20:29:57 -05:00
if data.skipTo:
2025-02-14 16:07:24 -05:00
queue_item = self.radio_util.get_queue_item_by_uuid(data.skipTo)
if not queue_item:
2025-02-15 21:09:33 -05:00
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'No such queue item.',
})
2025-02-14 16:07:24 -05:00
self.radio_util.active_playlist = self.radio_util.active_playlist[queue_item[0]:]
2025-02-11 20:01:07 -05:00
if not self.radio_util.active_playlist:
await self.radio_util.load_playlist()
2025-02-15 21:09:33 -05:00
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,
})
except Exception as e:
traceback.print_exc()
2025-02-15 21:09:33 -05:00
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'General failure.',
})
2025-02-10 20:29:57 -05:00
2025-02-15 21:09:33 -05:00
async def radio_reshuffle(self, data: ValidRadioReshuffleRequest,
request: Request) -> JSONResponse:
2025-02-10 20:29:57 -05:00
"""
Reshuffle the play queue
2025-02-16 08:17:27 -05:00
- **key**: API key
2025-02-10 20:29:57 -05:00
"""
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
2025-02-11 20:01:07 -05:00
random.shuffle(self.radio_util.active_playlist)
2025-02-15 21:09:33 -05:00
return JSONResponse(content={
2025-02-10 20:29:57 -05:00
'ok': True
2025-02-15 21:09:33 -05:00
})
2025-02-15 21:09:33 -05:00
async def radio_get_queue(self, request: Request,
limit: Optional[int] = 15_000) -> JSONResponse:
"""
2025-02-11 20:49:14 -05:00
Get current play queue, up to limit [default: 15k]
2025-02-16 08:17:27 -05:00
- **limit**: Number of queue items to return, default 15k
"""
2025-02-11 20:49:14 -05:00
queue_out: list[dict] = []
for x, item in enumerate(self.radio_util.active_playlist[0:limit]):
queue_out.append({
'pos': x,
2025-02-11 09:50:31 -05:00
'id': item.get('id'),
'uuid': item.get('uuid'),
'artist': item.get('artist'),
'song': item.get('song'),
'artistsong': item.get('artistsong'),
'duration': item.get('duration'),
})
2025-02-15 21:09:33 -05:00
return JSONResponse(content={
'items': queue_out
2025-02-15 21:09:33 -05:00
})
2025-02-15 21:09:33 -05:00
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]
2025-02-16 08:17:27 -05:00
- **key**: API key
- **uuid**: UUID to shift
- **next**: Play track next? If False, skips to the track
2025-02-15 21:09:33 -05:00
"""
2025-02-10 20:29:57 -05:00
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
2025-02-14 16:07:24 -05:00
queue_item = self.radio_util.get_queue_item_by_uuid(data.uuid)
if not queue_item:
2025-02-15 21:09:33 -05:00
return JSONResponse(status_code=500, content={
2025-02-14 16:07:24 -05:00
'err': True,
'errorText': 'Queue item not found.',
2025-02-15 21:09:33 -05:00
})
2025-02-14 16:07:24 -05:00
(x, item) = queue_item
2025-02-11 20:01:07 -05:00
self.radio_util.active_playlist.pop(x)
self.radio_util.active_playlist.insert(0, item)
2025-02-10 20:29:57 -05:00
if not data.next:
2025-02-11 20:01:07 -05:00
await self.radio_util._ls_skip()
2025-02-15 21:09:33 -05:00
return JSONResponse(content={
2025-02-10 20:29:57 -05:00
'ok': True,
2025-02-15 21:09:33 -05:00
})
2025-02-10 20:29:57 -05:00
2025-02-15 21:09:33 -05:00
async def radio_queue_remove(self, data: ValidRadioQueueRemovalRequest,
request: Request) -> JSONResponse:
"""
Remove an item from the current play queue
2025-02-16 08:17:27 -05:00
- **key**: API key
- **uuid**: UUID of queue item to remove
2025-02-15 21:09:33 -05:00
"""
2025-02-10 20:29:57 -05:00
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
2025-02-14 16:07:24 -05:00
queue_item = self.radio_util.get_queue_item_by_uuid(data.uuid)
if not queue_item:
2025-02-15 21:09:33 -05:00
return JSONResponse(status_code=500, content={
2025-02-14 16:07:24 -05:00
'err': True,
'errorText': 'Queue item not found.',
2025-02-15 21:09:33 -05:00
})
2025-02-14 16:07:24 -05:00
self.radio_util.active_playlist.pop(queue_item[0])
2025-02-15 21:09:33 -05:00
return JSONResponse(content={
2025-02-10 20:29:57 -05:00
'ok': True,
2025-02-15 21:09:33 -05:00
})
2025-02-10 20:29:57 -05:00
2025-02-14 16:07:24 -05:00
async def album_art_handler(self, request: Request, track_id: Optional[int] = None) -> Response:
2025-02-11 20:01:07 -05:00
"""
Get album art, optional parameter track_id may be specified.
Otherwise, current track album art will be pulled.
2025-02-16 08:17:27 -05:00
- **track_id**: Optional, if provided, will attempt to retrieve the album art of this track_id. Current track used otherwise.
2025-02-11 20:01:07 -05:00
"""
try:
2025-02-12 07:53:22 -05:00
if not track_id:
track_id = self.radio_util.now_playing.get('id')
2025-02-11 15:23:33 -05:00
logging.debug("Seeking album art with trackId: %s", track_id)
2025-02-12 07:53:22 -05:00
album_art: Optional[bytes] = await self.radio_util.get_album_art(track_id=track_id)
2025-02-11 09:50:31 -05:00
if not album_art:
return RedirectResponse(url="https://codey.lol/images/radio_art_default.jpg",
2025-02-11 09:50:31 -05:00
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)
2025-02-15 21:09:33 -05:00
async def radio_now_playing(self, request: Request) -> JSONResponse:
"""
Get currently playing track info
"""
2025-02-11 20:01:07 -05:00
ret_obj: dict = {**self.radio_util.now_playing}
try:
ret_obj['elapsed'] = int(time.time()) - ret_obj['start']
except KeyError:
traceback.print_exc()
ret_obj['elapsed'] = 0
ret_obj.pop('file_path')
2025-02-15 21:09:33 -05:00
return JSONResponse(content=ret_obj)
2025-02-10 20:29:57 -05:00
async def radio_get_next(self, data: ValidRadioNextRequest, request: Request,
2025-02-15 21:09:33 -05:00
background_tasks: BackgroundTasks) -> JSONResponse:
"""
Get next track
2025-02-11 20:01:07 -05:00
Track will be removed from the queue in the process.
2025-02-16 08:17:27 -05:00
- **key**: API key
- **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")
2025-02-11 20:01:07 -05:00
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()
2025-02-15 21:09:33 -05:00
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'General failure occurred, prompting playlist reload.',
})
2025-02-11 20:01:07 -05:00
next = self.radio_util.active_playlist.pop(0)
2025-02-11 15:21:01 -05:00
if not isinstance(next, dict):
2025-02-11 15:23:33 -05:00
logging.critical("next is of type: %s, reloading playlist...", type(next))
2025-02-11 20:01:07 -05:00
await self.radio_util.load_playlist()
await self.radio_util._ls_skip()
2025-02-15 21:09:33 -05:00
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'General failure occurred, prompting playlist reload.',
})
2025-02-11 15:21:01 -05:00
2025-02-11 20:01:07 -05:00
duration: int = next['duration']
time_started: int = int(time.time())
time_ends: int = int(time_started + duration)
2025-02-11 15:21:01 -05:00
2025-02-11 20:01:07 -05:00
if len(self.radio_util.active_playlist) > 1:
self.radio_util.active_playlist.append(next) # Push to end of playlist
else:
2025-02-11 20:01:07 -05:00
await self.radio_util.load_playlist()
2025-02-11 15:21:01 -05:00
2025-02-11 20:49:14 -05:00
self.radio_util.now_playing = next
2025-02-11 15:21:01 -05:00
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:
2025-02-11 20:01:07 -05:00
if not await self.radio_util.get_album_art(file_path=next['file_path']):
2025-02-12 07:53:22 -05:00
album_art = await self.radio_util.get_album_art(file_path=next['file_path'])
2025-02-15 21:09:33 -05:00
if 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'])
2025-02-11 15:21:01 -05:00
except:
traceback.print_exc()
2025-02-15 21:09:33 -05:00
return JSONResponse(content=next)
2025-02-15 21:09:33 -05:00
async def radio_request(self, data: ValidRadioSongRequest, request: Request) -> JSONResponse:
"""
Song request handler
2025-02-16 08:17:27 -05:00
- **key**: API key
- **artist**: Artist to search
- **song**: Song to search
- **artistsong**: Optional "Artist - Song" pair to search, in place of artist/song
- **alsoSkip**: If True, skips to the track; otherwise, track will be placed next up in queue
2025-02-15 21:09:33 -05:00
"""
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized")
2025-02-14 16:07:24 -05:00
artistsong: Optional[str] = data.artistsong
artist: Optional[str] = data.artist
song: Optional[str] = data.song
if artistsong and (artist or song):
2025-02-15 21:09:33 -05:00
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'Invalid request',
2025-02-15 21:09:33 -05:00
})
if not artistsong and (not artist or not song):
2025-02-15 21:09:33 -05:00
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'Invalid request',
2025-02-15 21:09:33 -05:00
})
2025-02-11 20:01:07 -05:00
search: bool = await self.radio_util.search_playlist(artistsong=artistsong,
artist=artist,
song=song)
if data.alsoSkip:
2025-02-11 20:01:07 -05:00
await self.radio_util._ls_skip()
2025-02-15 21:09:33 -05:00
return JSONResponse(content={
2025-02-11 20:01:07 -05:00
'result': search
2025-02-16 13:54:28 -05:00
})
async def radio_typeahead(self, data: ValidRadioTypeaheadRequest,
request: Request) -> JSONResponse:
"""
Radio typehead handler
- **query**: Typeahead query
"""
if not isinstance(data.query, str):
return JSONResponse(status_code=500, content={
'err': True,
'errorText': 'Invalid request.',
})
2025-02-16 14:04:59 -05:00
typeahead: Optional[list[str]] = await self.radio_util.trackdb_typeahead(data.query)
2025-02-16 13:54:28 -05:00
if not typeahead:
return JSONResponse(content=[])
return JSONResponse(content=typeahead)