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

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

View File

@ -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

View File

@ -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
)
}
)

View File

@ -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",
},
)

View File

@ -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)

View File

@ -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,
}
)

View File

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

View File

@ -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,
}
)

View File

@ -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
],
}
)

View File

@ -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],
}
)