rewrite pending; for now, additional support for multi-station
This commit is contained in:
@ -211,6 +211,7 @@ class ValidRadioSongRequest(BaseModel):
|
|||||||
song: Optional[str] = None
|
song: Optional[str] = None
|
||||||
artistsong: Optional[str] = None
|
artistsong: Optional[str] = None
|
||||||
alsoSkip: Optional[bool] = False
|
alsoSkip: Optional[bool] = False
|
||||||
|
station: str = "main"
|
||||||
|
|
||||||
|
|
||||||
class ValidRadioTypeaheadRequest(BaseModel):
|
class ValidRadioTypeaheadRequest(BaseModel):
|
||||||
|
@ -71,10 +71,12 @@ class LyricSearch(FastAPI):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for endpoint, handler in self.endpoints.items():
|
for endpoint, handler in self.endpoints.items():
|
||||||
rate_limit: tuple[int, int] = (2, 3) # Default; (Times, Seconds)
|
rate_limit: tuple[int, int] = (2, 3) # Default; (Times, Seconds)
|
||||||
_schema_include = endpoint in ["lyric/search"]
|
_schema_include = endpoint in ["lyric/search"]
|
||||||
|
|
||||||
if endpoint == "typeahead/lyrics": # More permissive rate limiting for typeahead
|
if (
|
||||||
|
endpoint == "typeahead/lyrics"
|
||||||
|
): # More permissive rate limiting for typeahead
|
||||||
rate_limit = (20, 2)
|
rate_limit = (20, 2)
|
||||||
(times, seconds) = rate_limit
|
(times, seconds) = rate_limit
|
||||||
|
|
||||||
@ -83,10 +85,11 @@ class LyricSearch(FastAPI):
|
|||||||
handler,
|
handler,
|
||||||
methods=["POST"],
|
methods=["POST"],
|
||||||
include_in_schema=_schema_include,
|
include_in_schema=_schema_include,
|
||||||
dependencies=[Depends(RateLimiter(times=times, seconds=seconds))] if not endpoint == "typeahead/lyrics" else None
|
dependencies=[Depends(RateLimiter(times=times, seconds=seconds))]
|
||||||
|
if not endpoint == "typeahead/lyrics"
|
||||||
|
else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def typeahead_handler(self, data: ValidTypeAheadRequest) -> JSONResponse:
|
async def typeahead_handler(self, data: ValidTypeAheadRequest) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Lyric search typeahead handler
|
Lyric search typeahead handler
|
||||||
|
@ -115,7 +115,7 @@ class Misc(FastAPI):
|
|||||||
with open(fallback_path, "rb") as f:
|
with open(fallback_path, "rb") as f:
|
||||||
return Response(content=f.read(), media_type="image/png")
|
return Response(content=f.read(), media_type="image/png")
|
||||||
|
|
||||||
async def get_radio_np(self) -> tuple[str, str, str]:
|
async def get_radio_np(self, station: str = "main") -> tuple[str, str, str]:
|
||||||
"""
|
"""
|
||||||
Get radio now playing
|
Get radio now playing
|
||||||
Args:
|
Args:
|
||||||
@ -124,7 +124,7 @@ class Misc(FastAPI):
|
|||||||
str: Radio now playing in artist - song format
|
str: Radio now playing in artist - song format
|
||||||
"""
|
"""
|
||||||
|
|
||||||
np: dict = self.radio.radio_util.now_playing
|
np: dict = self.radio.radio_util.now_playing[station]
|
||||||
artistsong: str = "N/A - N/A"
|
artistsong: str = "N/A - N/A"
|
||||||
artist = np.get("artist")
|
artist = np.get("artist")
|
||||||
song = np.get("song")
|
song = np.get("song")
|
||||||
@ -194,11 +194,11 @@ class Misc(FastAPI):
|
|||||||
)
|
)
|
||||||
return JSONResponse(content=found_counts)
|
return JSONResponse(content=found_counts)
|
||||||
|
|
||||||
async def homepage_radio_widget(self) -> JSONResponse:
|
async def homepage_radio_widget(self, station: str = "main") -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Homepage Radio Widget Handler
|
Homepage Radio Widget Handler
|
||||||
"""
|
"""
|
||||||
radio_np: tuple = await self.get_radio_np()
|
radio_np: tuple = await self.get_radio_np(station)
|
||||||
if not radio_np:
|
if not radio_np:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
|
@ -76,6 +76,7 @@ class Radio(FastAPI):
|
|||||||
Skip to the next track in the queue, or to uuid specified in skipTo if provided
|
Skip to the next track in the queue, or to uuid specified in skipTo if provided
|
||||||
- **key**: API key
|
- **key**: API key
|
||||||
- **skipTo**: Optional UUID to skip to
|
- **skipTo**: Optional UUID to skip to
|
||||||
|
- **station**: default "main"
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
||||||
@ -93,9 +94,7 @@ class Radio(FastAPI):
|
|||||||
self.radio_util.active_playlist[data.station] = self.radio_util.active_playlist[data.station][
|
self.radio_util.active_playlist[data.station] = self.radio_util.active_playlist[data.station][
|
||||||
queue_item[0] :
|
queue_item[0] :
|
||||||
]
|
]
|
||||||
# if not self.radio_util.active_playlist:
|
skip_result: bool = await self.radio_util._ls_skip(data.station)
|
||||||
# self.radio_util.load_playlist()
|
|
||||||
skip_result: bool = await self.radio_util._ls_skip()
|
|
||||||
status_code = 200 if skip_result else 500
|
status_code = 200 if skip_result else 500
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=status_code,
|
status_code=status_code,
|
||||||
@ -122,6 +121,7 @@ class Radio(FastAPI):
|
|||||||
"""
|
"""
|
||||||
Reshuffle the play queue
|
Reshuffle the play queue
|
||||||
- **key**: API key
|
- **key**: API key
|
||||||
|
- **station**: default "main"
|
||||||
"""
|
"""
|
||||||
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
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")
|
||||||
@ -187,6 +187,7 @@ class Radio(FastAPI):
|
|||||||
- **key**: API key
|
- **key**: API key
|
||||||
- **uuid**: UUID to shift
|
- **uuid**: UUID to shift
|
||||||
- **next**: Play track next? If False, skips to the track
|
- **next**: Play track next? If False, skips to the track
|
||||||
|
- **station**: default "main"
|
||||||
"""
|
"""
|
||||||
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
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")
|
||||||
@ -204,7 +205,7 @@ class Radio(FastAPI):
|
|||||||
self.radio_util.active_playlist[data.station].pop(x)
|
self.radio_util.active_playlist[data.station].pop(x)
|
||||||
self.radio_util.active_playlist[data.station].insert(0, item)
|
self.radio_util.active_playlist[data.station].insert(0, item)
|
||||||
if not data.next:
|
if not data.next:
|
||||||
await self.radio_util._ls_skip()
|
await self.radio_util._ls_skip(data.station)
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@ -218,6 +219,7 @@ class Radio(FastAPI):
|
|||||||
Remove an item from the current play queue
|
Remove an item from the current play queue
|
||||||
- **key**: API key
|
- **key**: API key
|
||||||
- **uuid**: UUID of queue item to remove
|
- **uuid**: UUID of queue item to remove
|
||||||
|
- **station**: default "main"
|
||||||
"""
|
"""
|
||||||
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
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")
|
||||||
@ -246,6 +248,7 @@ class Radio(FastAPI):
|
|||||||
Get album art, optional parameter track_id may be specified.
|
Get album art, optional parameter track_id may be specified.
|
||||||
Otherwise, current track album art will be pulled.
|
Otherwise, current track album art will be pulled.
|
||||||
- **track_id**: Optional, if provided, will attempt to retrieve the album art of this track_id. Current track used otherwise.
|
- **track_id**: Optional, if provided, will attempt to retrieve the album art of this track_id. Current track used otherwise.
|
||||||
|
- **station**: default "main"
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not track_id:
|
if not track_id:
|
||||||
@ -271,6 +274,7 @@ class Radio(FastAPI):
|
|||||||
station: Optional[str] = "main") -> JSONResponse:
|
station: Optional[str] = "main") -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Get currently playing track info
|
Get currently playing track info
|
||||||
|
- **station**: default "main"
|
||||||
"""
|
"""
|
||||||
ret_obj: dict = {**self.radio_util.now_playing[station]}
|
ret_obj: dict = {**self.radio_util.now_playing[station]}
|
||||||
ret_obj["station"] = station
|
ret_obj["station"] = station
|
||||||
@ -293,7 +297,7 @@ class Radio(FastAPI):
|
|||||||
(Track will be removed from the queue in the process.)
|
(Track will be removed from the queue in the process.)
|
||||||
- **key**: API key
|
- **key**: API key
|
||||||
- **skipTo**: Optional UUID to skip to
|
- **skipTo**: Optional UUID to skip to
|
||||||
- **station**: Station (default: "main")
|
- **station**: default: "main"
|
||||||
"""
|
"""
|
||||||
logging.info("Radio get next")
|
logging.info("Radio get next")
|
||||||
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
||||||
@ -337,8 +341,7 @@ class Radio(FastAPI):
|
|||||||
next["start"] = time_started
|
next["start"] = time_started
|
||||||
next["end"] = time_ends
|
next["end"] = time_ends
|
||||||
try:
|
try:
|
||||||
if data.station == "main":
|
background_tasks.add_task(self.radio_util.webhook_song_change, next, data.station)
|
||||||
background_tasks.add_task(self.radio_util.webhook_song_change, next)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.info("radio_get_next Exception: %s", str(e))
|
logging.info("radio_get_next Exception: %s", str(e))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -361,6 +364,7 @@ class Radio(FastAPI):
|
|||||||
- **song**: Song to search
|
- **song**: Song to search
|
||||||
- **artistsong**: Optional "Artist - Song" pair to search, in place of artist/song
|
- **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
|
- **alsoSkip**: If True, skips to the track; otherwise, track will be placed next up in queue
|
||||||
|
- **station**: default "main"
|
||||||
"""
|
"""
|
||||||
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
|
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")
|
||||||
@ -385,10 +389,10 @@ class Radio(FastAPI):
|
|||||||
)
|
)
|
||||||
|
|
||||||
search: bool = self.radio_util.search_db(
|
search: bool = self.radio_util.search_db(
|
||||||
artistsong=artistsong, artist=artist, song=song
|
artistsong=artistsong, artist=artist, song=song, station=data.station
|
||||||
)
|
)
|
||||||
if data.alsoSkip:
|
if data.alsoSkip:
|
||||||
await self.radio_util._ls_skip()
|
await self.radio_util._ls_skip(data.station)
|
||||||
return JSONResponse(content={"result": search})
|
return JSONResponse(content={"result": search})
|
||||||
|
|
||||||
def radio_typeahead(
|
def radio_typeahead(
|
||||||
|
@ -378,8 +378,13 @@ class RadioUtil:
|
|||||||
continue
|
continue
|
||||||
if station not in self.active_playlist:
|
if station not in self.active_playlist:
|
||||||
self.active_playlist[station] = []
|
self.active_playlist[station] = []
|
||||||
|
time_start = time.time()
|
||||||
|
logging.info("[%s] Running query: %s",
|
||||||
|
time_start, db_query)
|
||||||
db_cursor = db_conn.execute(db_query)
|
db_cursor = db_conn.execute(db_query)
|
||||||
results: list[sqlite3.Row] = db_cursor.fetchall()
|
results: list[sqlite3.Row] = db_cursor.fetchall()
|
||||||
|
time_end = time.time()
|
||||||
|
logging.info("[%s] Query completed; Time taken: %s", time_end, (time_end - time_start))
|
||||||
self.active_playlist[station] = [
|
self.active_playlist[station] = [
|
||||||
{
|
{
|
||||||
"uuid": str(uuid().hex),
|
"uuid": str(uuid().hex),
|
||||||
@ -402,7 +407,8 @@ class RadioUtil:
|
|||||||
station, len(self.active_playlist[station]),
|
station, len(self.active_playlist[station]),
|
||||||
)
|
)
|
||||||
|
|
||||||
random.shuffle(self.active_playlist[station])
|
if not station == "rock":# REMOVE ME, AFI RELATED
|
||||||
|
random.shuffle(self.active_playlist[station])
|
||||||
|
|
||||||
"""Dedupe"""
|
"""Dedupe"""
|
||||||
logging.info("Removing duplicate tracks...")
|
logging.info("Removing duplicate tracks...")
|
||||||
@ -441,7 +447,7 @@ class RadioUtil:
|
|||||||
station, len(self.active_playlist[station]),
|
station, len(self.active_playlist[station]),
|
||||||
)
|
)
|
||||||
self.playlists_loaded = True
|
self.playlists_loaded = True
|
||||||
self.loop.run_until_complete(self._ls_skip())
|
# self.loop.run_until_complete(self._ls_skip())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.info("Playlist load failed: %s", str(e))
|
logging.info("Playlist load failed: %s", str(e))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -521,22 +527,24 @@ class RadioUtil:
|
|||||||
return (x, item)
|
return (x, item)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _ls_skip(self) -> bool:
|
async def _ls_skip(self, station: str = "main") -> bool:
|
||||||
"""
|
"""
|
||||||
Ask LiquidSoap server to skip to the next track
|
Ask LiquidSoap server to skip to the next track
|
||||||
Args:
|
Args:
|
||||||
None
|
station (str): default "main"
|
||||||
Returns:
|
Returns:
|
||||||
bool
|
bool
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
async with session.get(
|
async with session.post(
|
||||||
f"{self.ls_uri}/next", timeout=ClientTimeout(connect=2, sock_read=2)
|
f"{self.ls_uri}/next",
|
||||||
|
data=station,
|
||||||
|
timeout=ClientTimeout(connect=2, sock_read=2)
|
||||||
) as request:
|
) as request:
|
||||||
request.raise_for_status()
|
request.raise_for_status()
|
||||||
text: Optional[str] = await request.text()
|
text: Optional[str] = await request.text()
|
||||||
return text == "OK"
|
return isinstance(text, str) and text.startswith("OK")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.debug("Skip failed: %s", str(e))
|
logging.debug("Skip failed: %s", str(e))
|
||||||
|
|
||||||
@ -558,15 +566,17 @@ class RadioUtil:
|
|||||||
return None
|
return None
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def webhook_song_change(self, track: dict) -> None:
|
async def webhook_song_change(self, track: dict, station: str = "main") -> None:
|
||||||
"""
|
"""
|
||||||
Handles Song Change Outbounds (Webhooks)
|
Handles Song Change Outbounds (Webhooks)
|
||||||
Args:
|
Args:
|
||||||
track (dict)
|
track (dict)
|
||||||
|
station (str): default "main"
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
return None # disabled temporarily (needs rate limiting)
|
||||||
# First, send track info
|
# First, send track info
|
||||||
"""
|
"""
|
||||||
TODO:
|
TODO:
|
||||||
@ -582,7 +592,7 @@ class RadioUtil:
|
|||||||
"username": "serious.FM",
|
"username": "serious.FM",
|
||||||
"embeds": [
|
"embeds": [
|
||||||
{
|
{
|
||||||
"title": "Now Playing",
|
"title": f"Now Playing on {station.title()}",
|
||||||
"description": f"## {track['song']}\nby\n## {track['artist']}",
|
"description": f"## {track['song']}\nby\n## {track['artist']}",
|
||||||
"color": 0x30C56F,
|
"color": 0x30C56F,
|
||||||
"thumbnail": {
|
"thumbnail": {
|
||||||
@ -636,36 +646,37 @@ class RadioUtil:
|
|||||||
) as request:
|
) as request:
|
||||||
request.raise_for_status()
|
request.raise_for_status()
|
||||||
|
|
||||||
# Next, AI feedback
|
# Next, AI feedback (for main stream only)
|
||||||
|
|
||||||
ai_response: Optional[str] = await self.get_ai_song_info(
|
if station == "main":
|
||||||
track["artist"], track["song"]
|
ai_response: Optional[str] = await self.get_ai_song_info(
|
||||||
)
|
track["artist"], track["song"]
|
||||||
if not ai_response:
|
)
|
||||||
return
|
if not ai_response:
|
||||||
|
return
|
||||||
|
|
||||||
hook_data = {
|
hook_data = {
|
||||||
"username": "GPT",
|
"username": "GPT",
|
||||||
"embeds": [
|
"embeds": [
|
||||||
{
|
{
|
||||||
"title": "AI Feedback",
|
"title": "AI Feedback",
|
||||||
"color": 0x35D0FF,
|
"color": 0x35D0FF,
|
||||||
"description": ai_response.strip(),
|
"description": ai_response.strip(),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
ai_hook: str = self.webhooks["gpt"].get("hook")
|
ai_hook: str = self.webhooks["gpt"].get("hook")
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
async with await session.post(
|
async with await session.post(
|
||||||
ai_hook,
|
ai_hook,
|
||||||
json=hook_data,
|
json=hook_data,
|
||||||
timeout=ClientTimeout(connect=5, sock_read=5),
|
timeout=ClientTimeout(connect=5, sock_read=5),
|
||||||
headers={
|
headers={
|
||||||
"content-type": "application/json; charset=utf-8",
|
"content-type": "application/json; charset=utf-8",
|
||||||
},
|
},
|
||||||
) as request:
|
) as request:
|
||||||
request.raise_for_status()
|
request.raise_for_status()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.info("Webhook error occurred: %s", str(e))
|
logging.info("Webhook error occurred: %s", str(e))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
Reference in New Issue
Block a user