radio_util: open tracks SQLite DB in readonly mode; black: reformat files
This commit is contained in:
@ -5,6 +5,7 @@ from pydantic import BaseModel
|
||||
Karma
|
||||
"""
|
||||
|
||||
|
||||
class ValidKarmaUpdateRequest(BaseModel):
|
||||
"""
|
||||
Requires authentication
|
||||
@ -25,35 +26,42 @@ class ValidKarmaRetrievalRequest(BaseModel):
|
||||
|
||||
keyword: str
|
||||
|
||||
|
||||
class ValidTopKarmaRequest(BaseModel):
|
||||
"""
|
||||
- **n**: Number of top results to return (default: 10)
|
||||
"""
|
||||
|
||||
n: Optional[int] = 10
|
||||
|
||||
|
||||
"""
|
||||
LastFM
|
||||
"""
|
||||
|
||||
|
||||
class LastFMException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ValidArtistSearchRequest(BaseModel):
|
||||
"""
|
||||
- **a**: artist name
|
||||
"""
|
||||
|
||||
a: str
|
||||
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"json_schema_extra": {
|
||||
"examples": [
|
||||
{
|
||||
"a": "eminem",
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ValidAlbumDetailRequest(BaseModel):
|
||||
"""
|
||||
- **a**: artist name
|
||||
@ -64,15 +72,17 @@ class ValidAlbumDetailRequest(BaseModel):
|
||||
release: str
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"json_schema_extra": {
|
||||
"examples": [
|
||||
{
|
||||
"a": "eminem",
|
||||
"release": "houdini",
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ValidTrackInfoRequest(BaseModel):
|
||||
"""
|
||||
- **a**: artist name
|
||||
@ -81,21 +91,24 @@ class ValidTrackInfoRequest(BaseModel):
|
||||
|
||||
a: str
|
||||
t: str
|
||||
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"json_schema_extra": {
|
||||
"examples": [
|
||||
{
|
||||
"a": "eminem",
|
||||
"t": "rap god",
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
Rand Msg
|
||||
"""
|
||||
|
||||
|
||||
class RandMsgRequest(BaseModel):
|
||||
"""
|
||||
- **short**: Short randmsg?
|
||||
@ -103,10 +116,12 @@ class RandMsgRequest(BaseModel):
|
||||
|
||||
short: Optional[bool] = False
|
||||
|
||||
|
||||
"""
|
||||
YT
|
||||
"""
|
||||
|
||||
|
||||
class ValidYTSearchRequest(BaseModel):
|
||||
"""
|
||||
- **t**: title to search
|
||||
@ -114,10 +129,12 @@ class ValidYTSearchRequest(BaseModel):
|
||||
|
||||
t: str = "rick astley - never gonna give you up"
|
||||
|
||||
|
||||
"""
|
||||
XC
|
||||
"""
|
||||
|
||||
|
||||
class ValidXCRequest(BaseModel):
|
||||
"""
|
||||
- **key**: valid XC API key
|
||||
@ -129,12 +146,14 @@ class ValidXCRequest(BaseModel):
|
||||
key: str
|
||||
bid: int
|
||||
cmd: str
|
||||
data: Optional[dict]
|
||||
data: Optional[dict]
|
||||
|
||||
|
||||
"""
|
||||
Transcriptions
|
||||
"""
|
||||
|
||||
|
||||
class ValidShowEpisodeListRequest(BaseModel):
|
||||
"""
|
||||
- **s**: show id
|
||||
@ -142,6 +161,7 @@ class ValidShowEpisodeListRequest(BaseModel):
|
||||
|
||||
s: int
|
||||
|
||||
|
||||
class ValidShowEpisodeLineRequest(BaseModel):
|
||||
"""
|
||||
- **s**: show id
|
||||
@ -151,10 +171,12 @@ class ValidShowEpisodeLineRequest(BaseModel):
|
||||
s: int
|
||||
e: int
|
||||
|
||||
|
||||
"""
|
||||
Lyric Search
|
||||
"""
|
||||
|
||||
|
||||
class ValidLyricRequest(BaseModel):
|
||||
"""
|
||||
- **a**: artist
|
||||
@ -167,7 +189,7 @@ class ValidLyricRequest(BaseModel):
|
||||
- **excluded_sources**: sources to exclude (new only)
|
||||
"""
|
||||
|
||||
a: Optional[str] = None
|
||||
a: Optional[str] = None
|
||||
s: Optional[str] = None
|
||||
t: Optional[str] = None
|
||||
sub: Optional[str] = None
|
||||
@ -178,32 +200,37 @@ class ValidLyricRequest(BaseModel):
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"examples": [
|
||||
{
|
||||
"a": "eminem",
|
||||
"s": "rap god",
|
||||
"src": "WEB",
|
||||
"extra": True,
|
||||
"lrc": False,
|
||||
"excluded_sources": [],
|
||||
}]
|
||||
}
|
||||
"examples": [
|
||||
{
|
||||
"a": "eminem",
|
||||
"s": "rap god",
|
||||
"src": "WEB",
|
||||
"extra": True,
|
||||
"lrc": False,
|
||||
"excluded_sources": [],
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ValidTypeAheadRequest(BaseModel):
|
||||
"""
|
||||
- **query**: query string
|
||||
"""
|
||||
|
||||
query: str
|
||||
|
||||
|
||||
"""
|
||||
Radio
|
||||
"""
|
||||
|
||||
|
||||
class RadioException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ValidRadioSongRequest(BaseModel):
|
||||
"""
|
||||
- **key**: API Key
|
||||
@ -212,55 +239,65 @@ class ValidRadioSongRequest(BaseModel):
|
||||
- **artistsong**: may be used IN PLACE OF artist/song to perform a combined/string search in the format "artist - song"
|
||||
- **alsoSkip**: Whether to skip immediately to this track [not implemented]
|
||||
"""
|
||||
|
||||
key: str
|
||||
artist: Optional[str] = None
|
||||
song: Optional[str] = None
|
||||
artistsong: Optional[str] = None
|
||||
alsoSkip: Optional[bool] = False
|
||||
|
||||
|
||||
|
||||
class ValidRadioTypeaheadRequest(BaseModel):
|
||||
"""
|
||||
- **query**: Typeahead query
|
||||
"""
|
||||
|
||||
query: str
|
||||
|
||||
|
||||
|
||||
|
||||
class ValidRadioQueueGetRequest(BaseModel):
|
||||
"""
|
||||
- **key**: API key (optional, needed if specifying a non-default limit)
|
||||
- **limit**: optional, default: 15k
|
||||
"""
|
||||
|
||||
|
||||
key: Optional[str] = None
|
||||
limit: Optional[int] = 15_000
|
||||
|
||||
|
||||
|
||||
class ValidRadioNextRequest(BaseModel):
|
||||
"""
|
||||
- **key**: API Key
|
||||
- **skipTo**: UUID to skip to [optional]
|
||||
"""
|
||||
|
||||
key: str
|
||||
skipTo: Optional[str] = None
|
||||
|
||||
|
||||
|
||||
class ValidRadioReshuffleRequest(ValidRadioNextRequest):
|
||||
"""
|
||||
- **key**: API Key
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class ValidRadioQueueShiftRequest(BaseModel):
|
||||
"""
|
||||
- **key**: API Key
|
||||
- **uuid**: UUID to shift
|
||||
- **uuid**: UUID to shift
|
||||
- **next**: Play next if true, immediately if false, default False
|
||||
"""
|
||||
|
||||
key: str
|
||||
uuid: str
|
||||
next: Optional[bool] = False
|
||||
|
||||
|
||||
|
||||
class ValidRadioQueueRemovalRequest(BaseModel):
|
||||
"""
|
||||
- **key**: API Key
|
||||
- **uuid**: UUID to remove
|
||||
- **uuid**: UUID to remove
|
||||
"""
|
||||
|
||||
key: str
|
||||
uuid: str
|
||||
uuid: str
|
||||
|
@ -7,16 +7,22 @@ import aiosqlite as sqlite3
|
||||
from typing import LiteralString, Optional, Union
|
||||
from fastapi import FastAPI, Request, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
from .constructors import (ValidTopKarmaRequest, ValidKarmaRetrievalRequest,
|
||||
ValidKarmaUpdateRequest)
|
||||
from .constructors import (
|
||||
ValidTopKarmaRequest,
|
||||
ValidKarmaRetrievalRequest,
|
||||
ValidKarmaUpdateRequest,
|
||||
)
|
||||
|
||||
|
||||
class KarmaDB:
|
||||
"""Karma DB Util"""
|
||||
def __init__(self) -> None:
|
||||
self.db_path: LiteralString = os.path.join("/", "usr", "local", "share",
|
||||
"sqlite_dbs", "karma.db")
|
||||
|
||||
async def get_karma(self, keyword: str) -> Union[int, dict]:
|
||||
def __init__(self) -> None:
|
||||
self.db_path: LiteralString = os.path.join(
|
||||
"/", "usr", "local", "share", "sqlite_dbs", "karma.db"
|
||||
)
|
||||
|
||||
async def get_karma(self, keyword: str) -> Union[int, dict]:
|
||||
"""Get Karma Value for Keyword
|
||||
Args:
|
||||
keyword (str): The keyword to search
|
||||
@ -24,16 +30,18 @@ class KarmaDB:
|
||||
Union[int, dict]
|
||||
"""
|
||||
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
|
||||
async with await db_conn.execute("SELECT score FROM karma WHERE keyword LIKE ? LIMIT 1", (keyword,)) as db_cursor:
|
||||
async with await db_conn.execute(
|
||||
"SELECT score FROM karma WHERE keyword LIKE ? LIMIT 1", (keyword,)
|
||||
) as db_cursor:
|
||||
try:
|
||||
(score,) = await db_cursor.fetchone()
|
||||
return score
|
||||
except TypeError:
|
||||
return {
|
||||
'err': True,
|
||||
'errorText': f'No records for {keyword}',
|
||||
"err": True,
|
||||
"errorText": f"No records for {keyword}",
|
||||
}
|
||||
|
||||
|
||||
async def get_top(self, n: Optional[int] = 10) -> Optional[list[tuple]]:
|
||||
"""
|
||||
Get Top n=10 Karma Entries
|
||||
@ -44,14 +52,17 @@ class KarmaDB:
|
||||
"""
|
||||
try:
|
||||
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
|
||||
async with await db_conn.execute("SELECT keyword, score FROM karma ORDER BY score DESC LIMIT ?", (n,)) as db_cursor:
|
||||
async with await db_conn.execute(
|
||||
"SELECT keyword, score FROM karma ORDER BY score DESC LIMIT ?", (n,)
|
||||
) as db_cursor:
|
||||
return await db_cursor.fetchall()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
async def update_karma(self, granter: str, keyword: str,
|
||||
flag: int) -> Optional[bool]:
|
||||
|
||||
async def update_karma(
|
||||
self, granter: str, keyword: str, flag: int
|
||||
) -> Optional[bool]:
|
||||
"""
|
||||
Update Karma for Keyword
|
||||
Args:
|
||||
@ -61,42 +72,71 @@ class KarmaDB:
|
||||
Returns:
|
||||
Optional[bool]
|
||||
"""
|
||||
|
||||
|
||||
if not flag in [0, 1]:
|
||||
return None
|
||||
|
||||
|
||||
modifier: str = "score + 1" if not flag else "score - 1"
|
||||
query: str = f"UPDATE karma SET score = {modifier}, last_change = ? WHERE keyword LIKE ?"
|
||||
new_keyword_query: str = "INSERT INTO karma(keyword, score, last_change) VALUES(?, ?, ?)"
|
||||
query: str = (
|
||||
f"UPDATE karma SET score = {modifier}, last_change = ? WHERE keyword LIKE ?"
|
||||
)
|
||||
new_keyword_query: str = (
|
||||
"INSERT INTO karma(keyword, score, last_change) VALUES(?, ?, ?)"
|
||||
)
|
||||
friendly_flag: str = "++" if not flag else "--"
|
||||
audit_message: str = f"{granter} adjusted karma for {keyword} @ {datetime.datetime.now().isoformat()}: {friendly_flag}"
|
||||
audit_query: str = "INSERT INTO karma_audit(impacted_keyword, comment) VALUES(?, ?)"
|
||||
audit_message: str = (
|
||||
f"{granter} adjusted karma for {keyword} @ {datetime.datetime.now().isoformat()}: {friendly_flag}"
|
||||
)
|
||||
audit_query: str = (
|
||||
"INSERT INTO karma_audit(impacted_keyword, comment) VALUES(?, ?)"
|
||||
)
|
||||
now: int = int(time.time())
|
||||
|
||||
logging.debug("Audit message: %s{audit_message}\nKeyword: %s{keyword}")
|
||||
|
||||
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
|
||||
async with await db_conn.execute(audit_query, (keyword, audit_message,)) as db_cursor:
|
||||
await db_conn.commit()
|
||||
async with await db_conn.execute(query, (now, keyword,)) as db_cursor:
|
||||
async with await db_conn.execute(
|
||||
audit_query,
|
||||
(
|
||||
keyword,
|
||||
audit_message,
|
||||
),
|
||||
) as db_cursor:
|
||||
await db_conn.commit()
|
||||
async with await db_conn.execute(
|
||||
query,
|
||||
(
|
||||
now,
|
||||
keyword,
|
||||
),
|
||||
) as db_cursor:
|
||||
if db_cursor.rowcount:
|
||||
await db_conn.commit()
|
||||
return True
|
||||
if db_cursor.rowcount < 1: # Keyword does not already exist
|
||||
await db_cursor.close()
|
||||
new_val = 1 if not flag else -1
|
||||
async with await db_conn.execute(new_keyword_query, (keyword, new_val, now,)) as db_cursor:
|
||||
async with await db_conn.execute(
|
||||
new_keyword_query,
|
||||
(
|
||||
keyword,
|
||||
new_val,
|
||||
now,
|
||||
),
|
||||
) as db_cursor:
|
||||
if db_cursor.rowcount >= 1:
|
||||
await db_conn.commit()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
|
||||
class Karma(FastAPI):
|
||||
"""
|
||||
Karma Endpoints
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, app: FastAPI, util, constants) -> None:
|
||||
self.app: FastAPI = app
|
||||
self.util = util
|
||||
@ -107,86 +147,111 @@ class Karma(FastAPI):
|
||||
"karma/get": self.get_karma_handler,
|
||||
"karma/modify": self.modify_karma_handler,
|
||||
"karma/top": self.top_karma_handler,
|
||||
}
|
||||
}
|
||||
|
||||
for endpoint, handler in self.endpoints.items():
|
||||
app.add_api_route(f"/{endpoint}", handler, methods=["POST"],
|
||||
include_in_schema=True)
|
||||
app.add_api_route(
|
||||
f"/{endpoint}", handler, methods=["POST"], include_in_schema=True
|
||||
)
|
||||
|
||||
|
||||
async def top_karma_handler(self, request: Request,
|
||||
data: Optional[ValidTopKarmaRequest] = None) -> JSONResponse:
|
||||
async def top_karma_handler(
|
||||
self, request: Request, data: Optional[ValidTopKarmaRequest] = None
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Get top keywords for karma
|
||||
- **n**: Number of top results to return (default: 10)
|
||||
"""
|
||||
|
||||
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')):
|
||||
if not self.util.check_key(
|
||||
request.url.path, request.headers.get("X-Authd-With")
|
||||
):
|
||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||
|
||||
|
||||
n: int = 10
|
||||
if data and data.n:
|
||||
n = int(data.n)
|
||||
|
||||
|
||||
try:
|
||||
top10: Optional[list[tuple]] = await self.db.get_top(n=n)
|
||||
if not top10:
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'General failure',
|
||||
})
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "General failure",
|
||||
},
|
||||
)
|
||||
return JSONResponse(content=top10)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Exception occurred.',
|
||||
})
|
||||
|
||||
async def get_karma_handler(self, data: ValidKarmaRetrievalRequest,
|
||||
request: Request) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Exception occurred.",
|
||||
},
|
||||
)
|
||||
|
||||
async def get_karma_handler(
|
||||
self, data: ValidKarmaRetrievalRequest, request: Request
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Get current karma value
|
||||
- **keyword**: Keyword to retrieve karma value for
|
||||
"""
|
||||
|
||||
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')):
|
||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||
|
||||
if not self.util.check_key(
|
||||
request.url.path, request.headers.get("X-Authd-With")
|
||||
):
|
||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||
|
||||
keyword: str = data.keyword
|
||||
try:
|
||||
count: Union[int, dict] = await self.db.get_karma(keyword)
|
||||
return JSONResponse(content={
|
||||
'keyword': keyword,
|
||||
'count': count,
|
||||
})
|
||||
return JSONResponse(
|
||||
content={
|
||||
"keyword": keyword,
|
||||
"count": count,
|
||||
}
|
||||
)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': "Exception occurred.",
|
||||
})
|
||||
|
||||
async def modify_karma_handler(self, data: ValidKarmaUpdateRequest,
|
||||
request: Request) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Exception occurred.",
|
||||
},
|
||||
)
|
||||
|
||||
async def modify_karma_handler(
|
||||
self, data: ValidKarmaUpdateRequest, request: Request
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Update karma count
|
||||
- **granter**: User who granted the karma
|
||||
- **keyword**: The keyword to modify
|
||||
- **flag**: 0 to decrement (--), 1 to increment (++)
|
||||
- **flag**: 0 to decrement (--), 1 to increment (++)
|
||||
"""
|
||||
|
||||
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With'), 2):
|
||||
if not self.util.check_key(
|
||||
request.url.path, request.headers.get("X-Authd-With"), 2
|
||||
):
|
||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||
|
||||
|
||||
if not data.flag in [0, 1]:
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Invalid request',
|
||||
})
|
||||
|
||||
return JSONResponse(content={
|
||||
'success': await self.db.update_karma(data.granter,
|
||||
data.keyword, data.flag)
|
||||
})
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Invalid request",
|
||||
},
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": await self.db.update_karma(
|
||||
data.granter, data.keyword, data.flag
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -3,13 +3,18 @@ import traceback
|
||||
from typing import Optional, Union
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
from .constructors import (ValidArtistSearchRequest, ValidAlbumDetailRequest,
|
||||
ValidTrackInfoRequest, LastFMException)
|
||||
|
||||
from .constructors import (
|
||||
ValidArtistSearchRequest,
|
||||
ValidAlbumDetailRequest,
|
||||
ValidTrackInfoRequest,
|
||||
LastFMException,
|
||||
)
|
||||
|
||||
|
||||
class LastFM(FastAPI):
|
||||
"""Last.FM Endpoints"""
|
||||
def __init__(self, app: FastAPI,
|
||||
util, constants) -> None:
|
||||
"""Last.FM Endpoints"""
|
||||
|
||||
def __init__(self, app: FastAPI, util, constants) -> None:
|
||||
self.app: FastAPI = app
|
||||
self.util = util
|
||||
self.constants = constants
|
||||
@ -21,72 +26,94 @@ class LastFM(FastAPI):
|
||||
"lastfm/get_release": self.release_detail_handler,
|
||||
"lastfm/get_release_tracklist": self.release_tracklist_handler,
|
||||
"lastfm/get_track_info": self.track_info_handler,
|
||||
#tbd
|
||||
}
|
||||
# tbd
|
||||
}
|
||||
|
||||
for endpoint, handler in self.endpoints.items():
|
||||
app.add_api_route(f"/{endpoint}", handler, methods=["POST"],
|
||||
include_in_schema=True)
|
||||
|
||||
async def artist_by_name_handler(self, data: ValidArtistSearchRequest) -> JSONResponse:
|
||||
app.add_api_route(
|
||||
f"/{endpoint}", handler, methods=["POST"], include_in_schema=True
|
||||
)
|
||||
|
||||
async def artist_by_name_handler(
|
||||
self, data: ValidArtistSearchRequest
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Get artist info
|
||||
- **a**: Artist to search
|
||||
"""
|
||||
artist: Optional[str] = data.a.strip()
|
||||
if not artist:
|
||||
return JSONResponse(content={
|
||||
'err': True,
|
||||
'errorText': 'No artist specified',
|
||||
})
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "No artist specified",
|
||||
}
|
||||
)
|
||||
|
||||
artist_result = await self.lastfm.search_artist(artist=artist)
|
||||
if not artist_result or not artist_result.get('bio')\
|
||||
or "err" in artist_result.keys():
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Search failed (no results?)',
|
||||
})
|
||||
|
||||
return JSONResponse(content={
|
||||
'success': True,
|
||||
'result': artist_result,
|
||||
})
|
||||
|
||||
async def artist_album_handler(self, data: ValidArtistSearchRequest) -> JSONResponse:
|
||||
if (
|
||||
not artist_result
|
||||
or not artist_result.get("bio")
|
||||
or "err" in artist_result.keys()
|
||||
):
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Search failed (no results?)",
|
||||
},
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
"result": artist_result,
|
||||
}
|
||||
)
|
||||
|
||||
async def artist_album_handler(
|
||||
self, data: ValidArtistSearchRequest
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Get artist's albums/releases
|
||||
- **a**: Artist to search
|
||||
"""
|
||||
artist: str = data.a.strip()
|
||||
if not artist:
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Invalid request: No artist specified',
|
||||
})
|
||||
|
||||
album_result: Union[dict, list[dict]] = await self.lastfm.get_artist_albums(artist=artist)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Invalid request: No artist specified",
|
||||
},
|
||||
)
|
||||
|
||||
album_result: Union[dict, list[dict]] = await self.lastfm.get_artist_albums(
|
||||
artist=artist
|
||||
)
|
||||
if isinstance(album_result, dict):
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'General failure.',
|
||||
})
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "General failure.",
|
||||
},
|
||||
)
|
||||
album_result_out: list = []
|
||||
seen_release_titles: list = []
|
||||
|
||||
|
||||
for release in album_result:
|
||||
release_title: str = release.get('title', 'Unknown')
|
||||
release_title: str = release.get("title", "Unknown")
|
||||
if release_title.lower() in seen_release_titles:
|
||||
continue
|
||||
seen_release_titles.append(release_title.lower())
|
||||
album_result_out.append(release)
|
||||
|
||||
return JSONResponse(content={
|
||||
'success': True,
|
||||
'result': album_result_out
|
||||
})
|
||||
return JSONResponse(content={"success": True, "result": album_result_out})
|
||||
|
||||
async def release_detail_handler(self, data: ValidAlbumDetailRequest) -> JSONResponse:
|
||||
async def release_detail_handler(
|
||||
self, data: ValidAlbumDetailRequest
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Get details of a particular release by an artist
|
||||
- **a**: Artist to search
|
||||
@ -94,85 +121,105 @@ class LastFM(FastAPI):
|
||||
"""
|
||||
artist: str = data.a.strip()
|
||||
release: str = data.release.strip()
|
||||
|
||||
|
||||
if not artist or not release:
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Invalid request',
|
||||
})
|
||||
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Invalid request",
|
||||
},
|
||||
)
|
||||
|
||||
release_result = await self.lastfm.get_release(artist=artist, album=release)
|
||||
ret_obj = {
|
||||
'id': release_result.get('id'),
|
||||
'artists': release_result.get('artists'),
|
||||
'title': release_result.get('title'),
|
||||
'summary': release_result.get('summary'),
|
||||
'tracks': release_result.get('tracks'),
|
||||
}
|
||||
"id": release_result.get("id"),
|
||||
"artists": release_result.get("artists"),
|
||||
"title": release_result.get("title"),
|
||||
"summary": release_result.get("summary"),
|
||||
"tracks": release_result.get("tracks"),
|
||||
}
|
||||
|
||||
return JSONResponse(content={
|
||||
'success': True,
|
||||
'result': ret_obj,
|
||||
})
|
||||
|
||||
async def release_tracklist_handler(self, data: ValidAlbumDetailRequest) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
"result": ret_obj,
|
||||
}
|
||||
)
|
||||
|
||||
async def release_tracklist_handler(
|
||||
self, data: ValidAlbumDetailRequest
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Get track list for a particular release by an artist
|
||||
- **a**: Artist to search
|
||||
- **release**: Release title to search
|
||||
- **release**: Release title to search
|
||||
"""
|
||||
artist: str = data.a.strip()
|
||||
release: str = data.release.strip()
|
||||
|
||||
|
||||
if not artist or not release:
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Invalid request',
|
||||
})
|
||||
|
||||
tracklist_result: dict = await self.lastfm.get_album_tracklist(artist=artist, album=release)
|
||||
return JSONResponse(content={
|
||||
'success': True,
|
||||
'id': tracklist_result.get('id'),
|
||||
'artists': tracklist_result.get('artists'),
|
||||
'title': tracklist_result.get('title'),
|
||||
'summary': tracklist_result.get('summary'),
|
||||
'tracks': tracklist_result.get('tracks'),
|
||||
})
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Invalid request",
|
||||
},
|
||||
)
|
||||
|
||||
tracklist_result: dict = await self.lastfm.get_album_tracklist(
|
||||
artist=artist, album=release
|
||||
)
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
"id": tracklist_result.get("id"),
|
||||
"artists": tracklist_result.get("artists"),
|
||||
"title": tracklist_result.get("title"),
|
||||
"summary": tracklist_result.get("summary"),
|
||||
"tracks": tracklist_result.get("tracks"),
|
||||
}
|
||||
)
|
||||
|
||||
async def track_info_handler(self, data: ValidTrackInfoRequest) -> JSONResponse:
|
||||
"""
|
||||
Get track info from Last.FM given an artist/track
|
||||
- **a**: Artist to search
|
||||
- **t**: Track title to search
|
||||
"""
|
||||
Get track info from Last.FM given an artist/track
|
||||
- **a**: Artist to search
|
||||
- **t**: Track title to search
|
||||
"""
|
||||
try:
|
||||
artist: str = data.a
|
||||
track: str = data.t
|
||||
|
||||
|
||||
if not artist or not track:
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Invalid request'
|
||||
})
|
||||
|
||||
track_info_result: Optional[dict] = await self.lastfm.get_track_info(artist=artist,
|
||||
track=track)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"err": True, "errorText": "Invalid request"},
|
||||
)
|
||||
|
||||
track_info_result: Optional[dict] = await self.lastfm.get_track_info(
|
||||
artist=artist, track=track
|
||||
)
|
||||
if not track_info_result:
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Not found.',
|
||||
})
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Not found.",
|
||||
},
|
||||
)
|
||||
if "err" in track_info_result:
|
||||
raise LastFMException("Unknown error occurred: %s",
|
||||
track_info_result.get('errorText', '??'))
|
||||
return JSONResponse(content={
|
||||
'success': True,
|
||||
'result': track_info_result
|
||||
})
|
||||
raise LastFMException(
|
||||
"Unknown error occurred: %s",
|
||||
track_info_result.get("errorText", "??"),
|
||||
)
|
||||
return JSONResponse(content={"success": True, "result": track_info_result})
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'General error',
|
||||
})
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "General error",
|
||||
},
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -2,20 +2,18 @@ import logging
|
||||
import time
|
||||
import os
|
||||
from typing import Optional, Annotated
|
||||
from fastapi import (
|
||||
FastAPI, Request, UploadFile,
|
||||
Response, HTTPException, Form
|
||||
)
|
||||
from fastapi import FastAPI, Request, UploadFile, Response, HTTPException, Form
|
||||
from fastapi.responses import JSONResponse
|
||||
import redis.asyncio as redis
|
||||
from lyric_search.sources import private, cache as LyricsCache, redis_cache
|
||||
|
||||
|
||||
class Misc(FastAPI):
|
||||
"""
|
||||
Misc Endpoints
|
||||
"""
|
||||
def __init__(self, app: FastAPI, my_util,
|
||||
constants, radio) -> None:
|
||||
|
||||
def __init__(self, app: FastAPI, my_util, constants, radio) -> None:
|
||||
self.app: FastAPI = app
|
||||
self.util = my_util
|
||||
self.constants = constants
|
||||
@ -28,47 +26,55 @@ class Misc(FastAPI):
|
||||
"widget/redis": self.homepage_redis_widget,
|
||||
"widget/sqlite": self.homepage_sqlite_widget,
|
||||
"widget/lyrics": self.homepage_lyrics_widget,
|
||||
"widget/radio": self.homepage_radio_widget,
|
||||
"widget/radio": self.homepage_radio_widget,
|
||||
"misc/get_activity_image": self.get_activity_image,
|
||||
}
|
||||
|
||||
|
||||
for endpoint, handler in self.endpoints.items():
|
||||
app.add_api_route(f"/{endpoint}", handler, methods=["GET"],
|
||||
include_in_schema=True)
|
||||
|
||||
app.add_api_route("/misc/upload_activity_image",
|
||||
self.upload_activity_image, methods=["POST"])
|
||||
|
||||
async def upload_activity_image(self,
|
||||
image: UploadFile,
|
||||
key: Annotated[str, Form()], request: Request) -> Response:
|
||||
if not key or not isinstance(key, str)\
|
||||
or not self.util.check_key(path=request.url.path, req_type=2, key=key):
|
||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||
app.add_api_route(
|
||||
f"/{endpoint}", handler, methods=["GET"], include_in_schema=True
|
||||
)
|
||||
|
||||
app.add_api_route(
|
||||
"/misc/upload_activity_image", self.upload_activity_image, methods=["POST"]
|
||||
)
|
||||
|
||||
async def upload_activity_image(
|
||||
self, image: UploadFile, key: Annotated[str, Form()], request: Request
|
||||
) -> Response:
|
||||
if (
|
||||
not key
|
||||
or not isinstance(key, str)
|
||||
or not self.util.check_key(path=request.url.path, req_type=2, key=key)
|
||||
):
|
||||
raise HTTPException(status_code=403, detail="Unauthorized")
|
||||
if not image:
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Invalid request',
|
||||
})
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Invalid request",
|
||||
},
|
||||
)
|
||||
self.activity_image = await image.read()
|
||||
return JSONResponse(content={
|
||||
'success': True,
|
||||
})
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
}
|
||||
)
|
||||
|
||||
async def get_activity_image(self, request: Request) -> Response:
|
||||
if isinstance(self.activity_image, bytes):
|
||||
return Response(content=self.activity_image,
|
||||
media_type="image/png")
|
||||
|
||||
|
||||
return Response(content=self.activity_image, media_type="image/png")
|
||||
|
||||
# Fallback
|
||||
fallback_path = os.path.join("/var/www/codey.lol/public",
|
||||
"images", "plex_placeholder.png")
|
||||
|
||||
with open(fallback_path, 'rb') as f:
|
||||
return Response(content=f.read(),
|
||||
media_type="image/png")
|
||||
|
||||
fallback_path = os.path.join(
|
||||
"/var/www/codey.lol/public", "images", "plex_placeholder.png"
|
||||
)
|
||||
|
||||
with open(fallback_path, "rb") as f:
|
||||
return Response(content=f.read(), media_type="image/png")
|
||||
|
||||
async def get_radio_np(self) -> tuple[str, str, str]:
|
||||
"""
|
||||
Get radio now playing
|
||||
@ -77,76 +83,94 @@ class Misc(FastAPI):
|
||||
Returns:
|
||||
str: Radio now playing in artist - song format
|
||||
"""
|
||||
|
||||
|
||||
np: dict = self.radio.radio_util.now_playing
|
||||
artistsong: str = np.get('artistsong', 'N/A - N/A')
|
||||
album: str = np.get('album', 'N/A')
|
||||
genre: str = np.get('genre', 'N/A')
|
||||
artistsong: str = np.get("artistsong", "N/A - N/A")
|
||||
album: str = np.get("album", "N/A")
|
||||
genre: str = np.get("genre", "N/A")
|
||||
return (artistsong, album, genre)
|
||||
|
||||
|
||||
|
||||
async def homepage_redis_widget(self) -> JSONResponse:
|
||||
"""
|
||||
Homepage Redis Widget Handler
|
||||
"""
|
||||
# Measure response time w/ test lyric search
|
||||
time_start: float = time.time() # Start time for response_time
|
||||
test_lyrics_result = await self.redis_client.ft().search("@artist: test @song: test")
|
||||
time_start: float = time.time() # Start time for response_time
|
||||
test_lyrics_result = await self.redis_client.ft().search(
|
||||
"@artist: test @song: test"
|
||||
)
|
||||
time_end: float = time.time()
|
||||
# End response time test
|
||||
total_keys = await self.redis_client.dbsize()
|
||||
response_time: float = time_end - time_start
|
||||
(_, ci_keys) = await self.redis_client.scan(cursor=0, match="ci_session*", count=10000000)
|
||||
response_time: float = time_end - time_start
|
||||
(_, ci_keys) = await self.redis_client.scan(
|
||||
cursor=0, match="ci_session*", count=10000000
|
||||
)
|
||||
num_ci_keys = len(ci_keys)
|
||||
index_info = await self.redis_client.ft().info()
|
||||
indexed_lyrics: int = index_info.get('num_docs')
|
||||
return JSONResponse(content={
|
||||
'responseTime': round(response_time, 7),
|
||||
'storedKeys': total_keys,
|
||||
'indexedLyrics': indexed_lyrics,
|
||||
'sessions': num_ci_keys,
|
||||
})
|
||||
|
||||
indexed_lyrics: int = index_info.get("num_docs")
|
||||
return JSONResponse(
|
||||
content={
|
||||
"responseTime": round(response_time, 7),
|
||||
"storedKeys": total_keys,
|
||||
"indexedLyrics": indexed_lyrics,
|
||||
"sessions": num_ci_keys,
|
||||
}
|
||||
)
|
||||
|
||||
async def homepage_sqlite_widget(self) -> JSONResponse:
|
||||
"""
|
||||
Homepage SQLite Widget Handler
|
||||
"""
|
||||
"""
|
||||
row_count: int = await self.lyr_cache.sqlite_rowcount()
|
||||
distinct_artists: int = await self.lyr_cache.sqlite_distinct("artist")
|
||||
lyrics_length: int = await self.lyr_cache.sqlite_lyrics_length()
|
||||
return JSONResponse(content={
|
||||
'storedRows': row_count,
|
||||
'distinctArtists': distinct_artists,
|
||||
'lyricsLength': lyrics_length,
|
||||
})
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"storedRows": row_count,
|
||||
"distinctArtists": distinct_artists,
|
||||
"lyricsLength": lyrics_length,
|
||||
}
|
||||
)
|
||||
|
||||
async def homepage_lyrics_widget(self) -> JSONResponse:
|
||||
"""
|
||||
Homepage Lyrics Widget Handler
|
||||
"""
|
||||
found_counts: Optional[dict] = await self.redis_cache.get_found_counts()
|
||||
if not isinstance(found_counts, dict):
|
||||
logging.info("DEBUG: Type of found counts from redis: %s\nContents: %s",
|
||||
type(found_counts), found_counts)
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'General failure.',
|
||||
})
|
||||
logging.info(
|
||||
"DEBUG: Type of found counts from redis: %s\nContents: %s",
|
||||
type(found_counts),
|
||||
found_counts,
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "General failure.",
|
||||
},
|
||||
)
|
||||
return JSONResponse(content=found_counts)
|
||||
|
||||
|
||||
async def homepage_radio_widget(self) -> JSONResponse:
|
||||
"""
|
||||
Homepage Radio Widget Handler
|
||||
"""
|
||||
radio_np: tuple = await self.get_radio_np()
|
||||
if not radio_np:
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'General failure.',
|
||||
})
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "General failure.",
|
||||
},
|
||||
)
|
||||
(artistsong, album, genre) = radio_np
|
||||
return JSONResponse(content={
|
||||
'now_playing': artistsong,
|
||||
'album': album,
|
||||
'genre': genre,
|
||||
})
|
||||
return JSONResponse(
|
||||
content={
|
||||
"now_playing": artistsong,
|
||||
"album": album,
|
||||
"genre": genre,
|
||||
}
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -6,20 +6,25 @@ from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
from .constructors import RandMsgRequest
|
||||
|
||||
|
||||
class RandMsg(FastAPI):
|
||||
"""
|
||||
Random Message 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.endpoint_name = "randmsg"
|
||||
|
||||
app.add_api_route(f"/{self.endpoint_name}", self.randmsg_handler, methods=["POST"])
|
||||
|
||||
async def randmsg_handler(self, data: Optional[RandMsgRequest] = None) -> JSONResponse:
|
||||
app.add_api_route(
|
||||
f"/{self.endpoint_name}", self.randmsg_handler, methods=["POST"]
|
||||
)
|
||||
|
||||
async def randmsg_handler(
|
||||
self, data: Optional[RandMsgRequest] = None
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Get a randomly generated message
|
||||
- **short**: Optional, if True, will limit length of returned random messages to <=126 characters (Discord restriction related)
|
||||
@ -35,58 +40,61 @@ class RandMsg(FastAPI):
|
||||
|
||||
match db_rand_selected:
|
||||
case 0:
|
||||
randmsg_db_path: Union[str, LiteralString] = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs", "qajoke.db") # For qajoke db
|
||||
db_query: str = "SELECT id, ('<b>Q:</b> ' || question || '<br/><b>A:</b> ' \
|
||||
|| answer) FROM jokes ORDER BY RANDOM() LIMIT 1" # For qajoke db
|
||||
randmsg_db_path: Union[str, LiteralString] = os.path.join(
|
||||
"/usr/local/share", "sqlite_dbs", "qajoke.db"
|
||||
) # For qajoke db
|
||||
db_query: str = (
|
||||
"SELECT id, ('<b>Q:</b> ' || question || '<br/><b>A:</b> ' \
|
||||
|| answer) FROM jokes ORDER BY RANDOM() LIMIT 1" # For qajoke db
|
||||
)
|
||||
title_attr = "QA Joke DB"
|
||||
case 1 | 9:
|
||||
randmsg_db_path = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs",
|
||||
"randmsg.db") # For randmsg db
|
||||
randmsg_db_path = os.path.join(
|
||||
"/usr/local/share", "sqlite_dbs", "randmsg.db"
|
||||
) # For randmsg db
|
||||
db_query = "SELECT id, msg FROM msgs WHERE \
|
||||
LENGTH(msg) <= 180 ORDER BY RANDOM() LIMIT 1" # For randmsg db
|
||||
LENGTH(msg) <= 180 ORDER BY RANDOM() LIMIT 1" # For randmsg db
|
||||
if db_rand_selected == 9:
|
||||
db_query = db_query.replace("<= 180", "<= 126")
|
||||
title_attr = "Random Msg DB"
|
||||
case 2:
|
||||
randmsg_db_path = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs",
|
||||
"trump.db") # For Trump Tweet DB
|
||||
randmsg_db_path = os.path.join(
|
||||
"/usr/local/share", "sqlite_dbs", "trump.db"
|
||||
) # For Trump Tweet DB
|
||||
db_query = "SELECT id, content FROM tweets \
|
||||
ORDER BY RANDOM() LIMIT 1" # For Trump Tweet DB
|
||||
ORDER BY RANDOM() LIMIT 1" # For Trump Tweet DB
|
||||
title_attr = "Trump Tweet DB"
|
||||
case 3:
|
||||
randmsg_db_path = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs",
|
||||
"philo.db") # For Philo DB
|
||||
randmsg_db_path = os.path.join(
|
||||
"/usr/local/share", "sqlite_dbs", "philo.db"
|
||||
) # For Philo DB
|
||||
db_query = "SELECT id, (content || '<br> - ' || speaker) FROM quotes \
|
||||
ORDER BY RANDOM() LIMIT 1" # For Philo DB
|
||||
title_attr = "Philosophical Quotes DB"
|
||||
case 4:
|
||||
randmsg_db_path = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs",
|
||||
"hate.db") # For Hate DB
|
||||
randmsg_db_path = os.path.join(
|
||||
"/usr/local/share", "sqlite_dbs", "hate.db"
|
||||
) # For Hate DB
|
||||
db_query = """SELECT id, ("<font color='#FF0000'>" || comment) FROM hate_speech \
|
||||
WHERE length(comment) <= 180 ORDER BY RANDOM() LIMIT 1"""
|
||||
title_attr = "Hate Speech DB"
|
||||
case 5:
|
||||
randmsg_db_path = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs",
|
||||
"rjokes.db") # r/jokes DB
|
||||
randmsg_db_path = os.path.join(
|
||||
"/usr/local/share", "sqlite_dbs", "rjokes.db"
|
||||
) # r/jokes DB
|
||||
db_query = """SELECT id, (title || "<br>" || body) FROM jokes \
|
||||
WHERE score >= 10000 ORDER BY RANDOM() LIMIT 1"""
|
||||
title_attr = "r/jokes DB"
|
||||
|
||||
|
||||
async with sqlite3.connect(database=randmsg_db_path, timeout=1) as _db:
|
||||
async with await _db.execute(db_query) as _cursor:
|
||||
result: sqlite3.Row = await _cursor.fetchone()
|
||||
(result_id, result_msg) = result
|
||||
result_msg = result_msg.strip()
|
||||
return JSONResponse(content=
|
||||
{
|
||||
"id": result_id,
|
||||
"msg": result_msg,
|
||||
"title": title_attr,
|
||||
})
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"id": result_id,
|
||||
"msg": result_msg,
|
||||
"title": title_attr,
|
||||
}
|
||||
)
|
||||
|
@ -5,10 +5,12 @@ from fastapi.responses import JSONResponse
|
||||
from typing import Optional, LiteralString, Union
|
||||
from .constructors import ValidShowEpisodeLineRequest, ValidShowEpisodeListRequest
|
||||
|
||||
|
||||
class Transcriptions(FastAPI):
|
||||
"""
|
||||
Transcription Endpoints
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, app: FastAPI, util, constants) -> None:
|
||||
self.app: FastAPI = app
|
||||
self.util = util
|
||||
@ -17,14 +19,17 @@ class Transcriptions(FastAPI):
|
||||
self.endpoints: dict = {
|
||||
"transcriptions/get_episodes": self.get_episodes_handler,
|
||||
"transcriptions/get_episode_lines": self.get_episode_lines_handler,
|
||||
#tbd
|
||||
}
|
||||
# tbd
|
||||
}
|
||||
|
||||
for endpoint, handler in self.endpoints.items():
|
||||
app.add_api_route(f"/{endpoint}", handler, methods=["POST"],
|
||||
include_in_schema=True)
|
||||
|
||||
async def get_episodes_handler(self, data: ValidShowEpisodeListRequest) -> JSONResponse:
|
||||
app.add_api_route(
|
||||
f"/{endpoint}", handler, methods=["POST"], include_in_schema=True
|
||||
)
|
||||
|
||||
async def get_episodes_handler(
|
||||
self, data: ValidShowEpisodeListRequest
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Get list of episodes by show id
|
||||
- **s**: Show ID to query
|
||||
@ -33,56 +38,68 @@ class Transcriptions(FastAPI):
|
||||
db_path: Optional[Union[str, LiteralString]] = None
|
||||
db_query: Optional[str] = None
|
||||
show_title: Optional[str] = None
|
||||
|
||||
|
||||
if not isinstance(show_id, int):
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Invalid request',
|
||||
})
|
||||
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Invalid request",
|
||||
},
|
||||
)
|
||||
|
||||
show_id = int(show_id)
|
||||
|
||||
if not(str(show_id).isnumeric()) or show_id not in [0, 1, 2]:
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Show not found.',
|
||||
})
|
||||
|
||||
|
||||
if not (str(show_id).isnumeric()) or show_id not in [0, 1, 2]:
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Show not found.",
|
||||
},
|
||||
)
|
||||
|
||||
match show_id:
|
||||
case 0:
|
||||
db_path = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs", "sp.db")
|
||||
db_path = os.path.join("/usr/local/share", "sqlite_dbs", "sp.db")
|
||||
db_query = """SELECT DISTINCT(("S" || Season || "E" || Episode || " " || Title)), ID FROM SP_DAT ORDER BY Season, Episode"""
|
||||
show_title = "South Park"
|
||||
case 1:
|
||||
db_path = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs", "futur.db")
|
||||
db_path = os.path.join("/usr/local/share", "sqlite_dbs", "futur.db")
|
||||
db_query = """SELECT DISTINCT(("S" || EP_S || "E" || EP_EP || " " || EP_TITLE)), EP_ID FROM clean_dialog ORDER BY EP_S, EP_EP"""
|
||||
show_title = "Futurama"
|
||||
case 2:
|
||||
db_path = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs", "parks.db")
|
||||
db_path = os.path.join("/usr/local/share", "sqlite_dbs", "parks.db")
|
||||
db_query = """SELECT DISTINCT(("S" || EP_S || "E" || EP_EP || " " || EP_TITLE)), EP_ID FROM clean_dialog ORDER BY EP_S, EP_EP"""
|
||||
show_title = "Parks And Rec"
|
||||
case _:
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Unknown error.',
|
||||
})
|
||||
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Unknown error.",
|
||||
},
|
||||
)
|
||||
|
||||
async with sqlite3.connect(database=db_path, timeout=1) as _db:
|
||||
async with await _db.execute(db_query) as _cursor:
|
||||
result: list[tuple] = await _cursor.fetchall()
|
||||
return JSONResponse(content={
|
||||
"show_title": show_title,
|
||||
"episodes": [
|
||||
{
|
||||
'id': item[1],
|
||||
'ep_friendly': item[0],
|
||||
} for item in result],
|
||||
})
|
||||
|
||||
async def get_episode_lines_handler(self, data: ValidShowEpisodeLineRequest) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
content={
|
||||
"show_title": show_title,
|
||||
"episodes": [
|
||||
{
|
||||
"id": item[1],
|
||||
"ep_friendly": item[0],
|
||||
}
|
||||
for item in result
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
async def get_episode_lines_handler(
|
||||
self, data: ValidShowEpisodeLineRequest
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Get lines for a particular episode
|
||||
- **s**: Show ID to query
|
||||
@ -90,39 +107,46 @@ class Transcriptions(FastAPI):
|
||||
"""
|
||||
show_id: int = int(data.s)
|
||||
episode_id: int = int(data.e)
|
||||
|
||||
|
||||
match show_id:
|
||||
case 0:
|
||||
db_path: Union[str, LiteralString] = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs", "sp.db")
|
||||
db_query: str = """SELECT ("S" || Season || "E" || Episode || " " || Title), Character, Line FROM SP_DAT WHERE ID = ?"""
|
||||
db_path: Union[str, LiteralString] = os.path.join(
|
||||
"/usr/local/share", "sqlite_dbs", "sp.db"
|
||||
)
|
||||
db_query: str = (
|
||||
"""SELECT ("S" || Season || "E" || Episode || " " || Title), Character, Line FROM SP_DAT WHERE ID = ?"""
|
||||
)
|
||||
case 1:
|
||||
db_path = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs", "futur.db")
|
||||
db_path = os.path.join("/usr/local/share", "sqlite_dbs", "futur.db")
|
||||
db_query = """SELECT ("S" || EP_S || "E" || EP_EP || " " || EP_TITLE || "<br><em>Opener: " || EP_OPENER || "</em>"), EP_LINE_SPEAKER, EP_LINE FROM clean_dialog WHERE EP_ID = ? ORDER BY LINE_ID ASC"""
|
||||
case 2:
|
||||
db_path = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs", "parks.db")
|
||||
db_path = os.path.join("/usr/local/share", "sqlite_dbs", "parks.db")
|
||||
db_query = """SELECT ("S" || EP_S || "E" || EP_EP || " " || EP_TITLE), EP_LINE_SPEAKER, EP_LINE FROM clean_dialog WHERE EP_ID = ? ORDER BY id ASC"""
|
||||
|
||||
|
||||
case _:
|
||||
return JSONResponse(status_code=500, content={
|
||||
'err': True,
|
||||
'errorText': 'Unknown error',
|
||||
})
|
||||
|
||||
async with sqlite3.connect(database=db_path,
|
||||
timeout=1) as _db:
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "Unknown error",
|
||||
},
|
||||
)
|
||||
|
||||
async with sqlite3.connect(database=db_path, timeout=1) as _db:
|
||||
params: tuple = (episode_id,)
|
||||
async with await _db.execute(db_query, params) as _cursor:
|
||||
result: list[tuple] = await _cursor.fetchall()
|
||||
first_result: tuple = result[0]
|
||||
return JSONResponse(content={
|
||||
'episode_id': episode_id,
|
||||
'ep_friendly': first_result[0].strip(),
|
||||
'lines': [
|
||||
{
|
||||
'speaker': item[1].strip(),
|
||||
'line': item[2].strip(),
|
||||
} for item in result],
|
||||
})
|
||||
return JSONResponse(
|
||||
content={
|
||||
"episode_id": episode_id,
|
||||
"ep_friendly": first_result[0].strip(),
|
||||
"lines": [
|
||||
{
|
||||
"speaker": item[1].strip(),
|
||||
"line": item[2].strip(),
|
||||
}
|
||||
for item in result
|
||||
],
|
||||
}
|
||||
)
|
||||
|
@ -4,12 +4,13 @@ from fastapi.responses import JSONResponse
|
||||
from typing import Optional, Union
|
||||
from .constructors import ValidYTSearchRequest
|
||||
|
||||
|
||||
class YT(FastAPI):
|
||||
"""
|
||||
YT Endpoints
|
||||
"""
|
||||
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
|
||||
@ -17,28 +18,34 @@ class YT(FastAPI):
|
||||
|
||||
self.endpoints: dict = {
|
||||
"yt/search": self.yt_video_search_handler,
|
||||
}
|
||||
}
|
||||
|
||||
for endpoint, handler in self.endpoints.items():
|
||||
app.add_api_route(f"/{endpoint}", handler, methods=["POST"],
|
||||
include_in_schema=True)
|
||||
|
||||
app.add_api_route(
|
||||
f"/{endpoint}", handler, methods=["POST"], include_in_schema=True
|
||||
)
|
||||
|
||||
async def yt_video_search_handler(self, data: ValidYTSearchRequest) -> JSONResponse:
|
||||
"""
|
||||
Search for YT Video by Title (closest match returned)
|
||||
- **t**: Title to search
|
||||
"""
|
||||
|
||||
|
||||
title: str = data.t
|
||||
yts_res: Optional[list[dict]] = await self.ytsearch.search(title)
|
||||
if not yts_res:
|
||||
return JSONResponse(status_code=404, content={
|
||||
'err': True,
|
||||
'errorText': 'No result.',
|
||||
})
|
||||
yt_video_id: Union[str, bool] = yts_res[0].get('id', False)
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={
|
||||
"err": True,
|
||||
"errorText": "No result.",
|
||||
},
|
||||
)
|
||||
yt_video_id: Union[str, bool] = yts_res[0].get("id", False)
|
||||
|
||||
return JSONResponse(content={
|
||||
'video_id': yt_video_id,
|
||||
'extras': yts_res[0],
|
||||
})
|
||||
return JSONResponse(
|
||||
content={
|
||||
"video_id": yt_video_id,
|
||||
"extras": yts_res[0],
|
||||
}
|
||||
)
|
||||
|
Reference in New Issue
Block a user