This commit is contained in:
codey 2025-02-14 16:07:24 -05:00
parent 00af36703a
commit 60416c493f
19 changed files with 204 additions and 308 deletions

26
base.py
View File

@ -25,8 +25,6 @@ app = FastAPI(title="codey.lol API",
constants = importlib.import_module("constants").Constants() constants = importlib.import_module("constants").Constants()
util = importlib.import_module("util").Utilities(app, constants) util = importlib.import_module("util").Utilities(app, constants)
glob_state = importlib.import_module("state").State(app, util, constants)
origins = [ origins = [
"https://codey.lol", "https://codey.lol",
@ -83,23 +81,23 @@ End Blacklisted Routes
Actionable Routes Actionable Routes
""" """
randmsg_endpoint = importlib.import_module("endpoints.rand_msg").RandMsg(app, util, constants, glob_state) randmsg_endpoint = importlib.import_module("endpoints.rand_msg").RandMsg(app, util, constants)
transcription_endpoints = importlib.import_module("endpoints.transcriptions").Transcriptions(app, util, constants, glob_state) transcription_endpoints = importlib.import_module("endpoints.transcriptions").Transcriptions(app, util, constants)
ai_endpoints = importlib.import_module("endpoints.ai").AI(app, util, constants, glob_state) ai_endpoints = importlib.import_module("endpoints.ai").AI(app, util, constants)
# Below also provides: /lyric_cache_list/ (in addition to /lyric_search/) # Below also provides: /lyric_cache_list/ (in addition to /lyric_search/)
lyric_search_endpoint = importlib.import_module("endpoints.lyric_search").LyricSearch(app, util, constants, glob_state) lyric_search_endpoint = importlib.import_module("endpoints.lyric_search").LyricSearch(app, util, constants)
# Below provides numerous LastFM-fed endpoints # Below provides numerous LastFM-fed endpoints
lastfm_endpoints = importlib.import_module("endpoints.lastfm").LastFM(app, util, constants, glob_state) lastfm_endpoints = importlib.import_module("endpoints.lastfm").LastFM(app, util, constants)
# Below: YT endpoint(s) # Below: YT endpoint(s)
yt_endpoints = importlib.import_module("endpoints.yt").YT(app, util, constants, glob_state) yt_endpoints = importlib.import_module("endpoints.yt").YT(app, util, constants)
# Below: XC endpoint(s) # Below: XC endpoint(s)
xc_endpoints = importlib.import_module("endpoints.xc").XC(app, util, constants, glob_state) xc_endpoints = importlib.import_module("endpoints.xc").XC(app, util, constants)
# Below: Karma endpoint(s) # Below: Karma endpoint(s)
karma_endpoints = importlib.import_module("endpoints.karma").Karma(app, util, constants, glob_state) karma_endpoints = importlib.import_module("endpoints.karma").Karma(app, util, constants)
# Below: Radio endpoint(s) - in development, sporadically loaded as needed # Below: Radio endpoint(s) - in development, sporadically loaded as needed
radio_endpoints = importlib.import_module("endpoints.radio").Radio(app, util, constants, glob_state) radio_endpoints = importlib.import_module("endpoints.radio").Radio(app, util, constants)
# Below: Misc endpoints # Below: Misc endpoints
misc_endpoints = importlib.import_module("endpoints.misc").Misc(app, util, constants, glob_state, radio_endpoints) misc_endpoints = importlib.import_module("endpoints.misc").Misc(app, util, constants, radio_endpoints)
""" """
End Actionable Routes End Actionable Routes
@ -109,6 +107,6 @@ End Actionable Routes
""" """
Startup Startup
""" """
redis_cache = redis_cache.RedisCache() redis = redis_cache.RedisCache()
asyncio.get_event_loop().create_task( asyncio.get_event_loop().create_task(
redis_cache.create_index()) redis.create_index())

View File

@ -4,19 +4,20 @@
import logging import logging
import traceback import traceback
import regex import regex
from regex import Pattern
from typing import Union
from aiohttp import ClientSession, ClientTimeout from aiohttp import ClientSession, ClientTimeout
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
from .constructors import ValidHookSongRequest, ValidAISongRequest from .constructors import ValidHookSongRequest, ValidAISongRequest
class AI(FastAPI): class AI(FastAPI):
"""AI Endpoints""" """AI Endpoints"""
def __init__(self, app: FastAPI, my_util, constants, glob_state): # pylint: disable=super-init-not-called def __init__(self, app: FastAPI, my_util, constants): # pylint: disable=super-init-not-called
self.app = app self.app = app
self.util = my_util self.util = my_util
self.constants = constants self.constants = constants
self.glob_state = glob_state self.url_clean_regex: Pattern = regex.compile(r'^\/ai\/(openai|base)\/')
self.url_clean_regex = regex.compile(r'^\/ai\/(openai|base)\/') self.endpoints: dict = {
self.endpoints = {
"ai/openai": self.ai_openai_handler, "ai/openai": self.ai_openai_handler,
"ai/base": self.ai_handler, "ai/base": self.ai_handler,
"ai/song": self.ai_song_handler, "ai/song": self.ai_song_handler,
@ -29,16 +30,20 @@ class AI(FastAPI):
include_in_schema=False) include_in_schema=False)
async def respond_via_webhook(self, data: ValidHookSongRequest, originalRequest: Request): async def respond_via_webhook(self, data: ValidHookSongRequest, originalRequest: Request) -> bool:
"""Respond via Webhook""" """Respond via Webhook"""
try: try:
logging.debug("Request received: %s", data) logging.debug("Request received: %s", data)
data2 = data.copy() data2 = data.copy()
del data2.hook del data2.hook
if not data.hook:
return False
response = await self.ai_song_handler(data2, originalRequest) response = await self.ai_song_handler(data2, originalRequest)
if not response.get('resp'): if not response.get('resp'):
logging.critical("NO RESP!") logging.critical("NO RESP!")
return return False
response = response.get('resp') response = response.get('resp')
hook_data = { hook_data = {
'username': 'Claude', 'username': 'Claude',
@ -82,7 +87,6 @@ class AI(FastAPI):
json=await request.json(), json=await request.json(),
headers=local_llm_headers, headers=local_llm_headers,
timeout=ClientTimeout(connect=15, sock_read=30)) as out_request: timeout=ClientTimeout(connect=15, sock_read=30)) as out_request:
await self.glob_state.increment_counter('ai_requests')
response = await out_request.json() response = await out_request.json()
return response return response
except Exception as e: # pylint: disable=broad-exception-caught except Exception as e: # pylint: disable=broad-exception-caught
@ -117,7 +121,6 @@ class AI(FastAPI):
json=await request.json(), json=await request.json(),
headers=local_llm_headers, headers=local_llm_headers,
timeout=ClientTimeout(connect=15, sock_read=30)) as out_request: timeout=ClientTimeout(connect=15, sock_read=30)) as out_request:
await self.glob_state.increment_counter('ai_requests')
response = await out_request.json() response = await out_request.json()
return response return response
except Exception as e: # pylint: disable=broad-exception-caught except Exception as e: # pylint: disable=broad-exception-caught
@ -134,7 +137,7 @@ class AI(FastAPI):
'success': True, 'success': True,
} }
async def ai_song_handler(self, data: ValidAISongRequest, request: Request): async def ai_song_handler(self, data: Union[ValidAISongRequest, ValidHookSongRequest], request: Request):
""" """
/ai/song /ai/song
AI (Song Info) Request [Public] AI (Song Info) Request [Public]
@ -165,9 +168,8 @@ class AI(FastAPI):
async with await session.post('https://api.anthropic.com/v1/messages', async with await session.post('https://api.anthropic.com/v1/messages',
json=request_data, json=request_data,
headers=local_llm_headers, headers=local_llm_headers,
timeout=ClientTimeout(connect=15, sock_read=30)) as request: timeout=ClientTimeout(connect=15, sock_read=30)) as aiohttp_request:
await self.glob_state.increment_counter('claude_ai_requests') response = await aiohttp_request.json()
response = await request.json()
logging.debug("Response: %s", logging.debug("Response: %s",
response) response)
if response.get('type') == 'error': if response.get('type') == 'error':

View File

@ -14,7 +14,7 @@ from .constructors import ValidTopKarmaRequest, ValidKarmaRetrievalRequest,\
class KarmaDB: class KarmaDB:
"""Karma DB Util""" """Karma DB Util"""
def __init__(self): def __init__(self) -> None:
self.db_path: LiteralString = os.path.join("/", "usr", "local", "share", self.db_path: LiteralString = os.path.join("/", "usr", "local", "share",
"sqlite_dbs", "karma.db") "sqlite_dbs", "karma.db")
@ -36,7 +36,7 @@ class KarmaDB:
'errorText': f'No records for {keyword}', 'errorText': f'No records for {keyword}',
} }
async def get_top(self, n: Optional[int] = 10) -> list[tuple]: async def get_top(self, n: Optional[int] = 10) -> Optional[list[tuple]]:
"""Get Top n=10 Karma Entries """Get Top n=10 Karma Entries
Args: Args:
n (Optional[int]) = 10: The number of top results to return n (Optional[int]) = 10: The number of top results to return
@ -49,7 +49,7 @@ class KarmaDB:
return await db_cursor.fetchall() return await db_cursor.fetchall()
except: except:
traceback.print_exc() traceback.print_exc()
return 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 """Update Karma for Keyword
@ -62,7 +62,7 @@ class KarmaDB:
""" """
if not flag in [0, 1]: if not flag in [0, 1]:
return return None
modifier: str = "score + 1" if not flag else "score - 1" modifier: str = "score + 1" if not flag else "score - 1"
query: str = f"UPDATE karma SET score = {modifier}, last_change = ? WHERE keyword LIKE ?" query: str = f"UPDATE karma SET score = {modifier}, last_change = ? WHERE keyword LIKE ?"
@ -90,13 +90,14 @@ class KarmaDB:
return True return True
else: else:
return False return False
return False
class Karma(FastAPI): class Karma(FastAPI):
"""Karma Endpoints""" """Karma Endpoints"""
def __init__(self, app: FastAPI, util, constants, glob_state): # pylint: disable=super-init-not-called def __init__(self, app: FastAPI, util, constants): # pylint: disable=super-init-not-called
self.app = app self.app = app
self.util = util self.util = util
self.constants = constants self.constants = constants
self.glob_state = glob_state
self.db = KarmaDB() self.db = KarmaDB()
self.endpoints: dict = { self.endpoints: dict = {
@ -117,12 +118,17 @@ class Karma(FastAPI):
raise HTTPException(status_code=403, detail="Unauthorized") raise HTTPException(status_code=403, detail="Unauthorized")
n: int = 10 n: int = 10
if data: if data and data.n:
n: int = int(data.n) n = int(data.n)
try: try:
top10: list[tuple] = await self.db.get_top(n=n) top10: Optional[list[tuple]] = await self.db.get_top(n=n)
if not top10:
return {
'err': True,
'errorText': 'General failure',
}
return top10 return top10
except: except:
traceback.print_exc() traceback.print_exc()

View File

@ -10,14 +10,13 @@ from .constructors import ValidArtistSearchRequest, ValidAlbumDetailRequest,\
class LastFM(FastAPI): class LastFM(FastAPI):
"""Last.FM Endpoints""" """Last.FM Endpoints"""
def __init__(self, app: FastAPI, util, constants, glob_state) -> None: # pylint: disable=super-init-not-called def __init__(self, app: FastAPI, util, constants) -> None: # pylint: disable=super-init-not-called
self.app = app self.app = app
self.util = util self.util = util
self.constants = constants self.constants = constants
self.glob_state = glob_state
self.lastfm = importlib.import_module("lastfm_wrapper").LastFM() self.lastfm = importlib.import_module("lastfm_wrapper").LastFM()
self.endpoints = { self.endpoints: dict = {
"lastfm/get_artist_by_name": self.artist_by_name_handler, "lastfm/get_artist_by_name": self.artist_by_name_handler,
"lastfm/get_artist_albums": self.artist_album_handler, "lastfm/get_artist_albums": self.artist_album_handler,
"lastfm/get_release": self.release_detail_handler, "lastfm/get_release": self.release_detail_handler,
@ -71,7 +70,7 @@ class LastFM(FastAPI):
seen_release_titles: list = [] seen_release_titles: list = []
for release in album_result: for release in album_result:
release_title: str = release.get('title') release_title: str = release.get('title', 'Unknown')
if release_title.lower() in seen_release_titles: if release_title.lower() in seen_release_titles:
continue continue
seen_release_titles.append(release_title.lower()) seen_release_titles.append(release_title.lower())

View File

@ -7,7 +7,8 @@ import urllib.parse
import regex import regex
import aiosqlite as sqlite3 import aiosqlite as sqlite3
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from typing import LiteralString, Optional, Pattern from typing import LiteralString, Optional, Callable
from regex import Pattern
from .constructors import ValidTypeAheadRequest, ValidLyricRequest from .constructors import ValidTypeAheadRequest, ValidLyricRequest
from lyric_search.constructors import LyricsResult from lyric_search.constructors import LyricsResult
from lyric_search.sources import aggregate from lyric_search.sources import aggregate
@ -15,32 +16,31 @@ from lyric_search import notifier
class CacheUtils: class CacheUtils:
"""Lyrics Cache DB Utils""" """Lyrics Cache DB Utils"""
def __init__(self): def __init__(self) -> None:
self.lyrics_db_path: LiteralString = os.path.join("/", "usr", "local", "share", self.lyrics_db_path: LiteralString = os.path.join("/usr/local/share",
"sqlite_dbs", "cached_lyrics.db") "sqlite_dbs", "cached_lyrics.db")
async def check_typeahead(self, s: str, pre_query: str | None = None) -> Optional[list[dict]]: async def check_typeahead(self, s: str, pre_query: str | None = None) -> list[dict]:
"""Check s against artists stored - for typeahead""" """Check s against artists stored - for typeahead"""
async with sqlite3.connect(self.lyrics_db_path, async with sqlite3.connect(self.lyrics_db_path,
timeout=2) as db_conn: timeout=2) as db_conn:
db_conn.row_factory = lambda c, r: dict([(col[0], r[idx]) for idx, col in enumerate(c.description)]) db_conn.row_factory = sqlite3.Row
if not pre_query: if not pre_query:
query: str = "SELECT distinct(artist) FROM lyrics WHERE artist LIKE ? LIMIT 15" query: str = "SELECT distinct(artist) FROM lyrics WHERE artist LIKE ? LIMIT 15"
query_params: tuple = (f"%{s}%",) query_params: tuple = (f"%{s}%",)
else: else:
query: str = "SELECT distinct(song) FROM lyrics WHERE artist LIKE ? AND song LIKE ? LIMIT 15" query = "SELECT distinct(song) FROM lyrics WHERE artist LIKE ? AND song LIKE ? LIMIT 15"
query_params: tuple = (f"%{pre_query}%", f"%{s}%",) query_params = (f"%{pre_query}%", f"%{s}%",)
async with await db_conn.execute(query, query_params) as db_cursor: async with await db_conn.execute(query, query_params) as db_cursor:
return await db_cursor.fetchall() return await db_cursor.fetchall()
class LyricSearch(FastAPI): class LyricSearch(FastAPI):
"""Lyric Search Endpoint""" """Lyric Search Endpoint"""
def __init__(self, app: FastAPI, util, constants, glob_state): # pylint: disable=super-init-not-called def __init__(self, app: FastAPI, util, constants): # pylint: disable=super-init-not-called
self.app = app self.app = app
self.util = util self.util = util
self.constants = constants self.constants = constants
self.glob_state = glob_state
self.cache_utils = CacheUtils() self.cache_utils = CacheUtils()
self.notifier = notifier.DiscordNotifier() self.notifier = notifier.DiscordNotifier()
@ -78,7 +78,7 @@ class LyricSearch(FastAPI):
'errorText': 'Invalid request', 'errorText': 'Invalid request',
} }
query: str = data.query query: str = data.query
typeahead_result: Optional[list[dict]] = await self.cache_utils.check_typeahead(query) typeahead_result: list[dict] = await self.cache_utils.check_typeahead(query)
typeahead_list: list[str] = [str(r.get('artist')) for r in typeahead_result] typeahead_list: list[str] = [str(r.get('artist')) for r in typeahead_result]
return typeahead_list return typeahead_list
@ -92,7 +92,7 @@ class LyricSearch(FastAPI):
} }
pre_query: str = data.pre_query pre_query: str = data.pre_query
query: str = data.query query: str = data.query
typeahead_result: Optional[list[dict]] = await self.cache_utils.check_typeahead(query, pre_query) typeahead_result: list[dict] = await self.cache_utils.check_typeahead(query, pre_query)
typeahead_list: list[str] = [str(r.get('song')) for r in typeahead_result] typeahead_list: list[str] = [str(r.get('song')) for r in typeahead_result]
return typeahead_list return typeahead_list
@ -122,24 +122,30 @@ class LyricSearch(FastAPI):
} }
if not data.t: if not data.t:
search_artist: str = data.a search_artist: Optional[str] = data.a
search_song: str = data.s search_song: Optional[str] = data.s
else: else:
t_split = data.t.split(" - ", maxsplit=1) t_split = data.t.split(" - ", maxsplit=1)
search_artist: str = t_split[0] search_artist = t_split[0]
search_song: str = t_split[1] search_song = t_split[1]
if search_artist and search_song: if search_artist and search_song:
search_artist: str = self.constants.DOUBLE_SPACE_REGEX.sub(" ", search_artist.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_song = str(self.constants.DOUBLE_SPACE_REGEX.sub(" ", search_song.strip()))
search_artist: str = urllib.parse.unquote(search_artist) search_artist = urllib.parse.unquote(search_artist)
search_song: str = urllib.parse.unquote(search_song) search_song = urllib.parse.unquote(search_song)
excluded_sources: list = data.excluded_sources if not isinstance(search_artist, str) or not isinstance(search_song, str):
return {
'err': True,
'errorText': 'Invalid request',
}
excluded_sources: Optional[list] = data.excluded_sources
aggregate_search = aggregate.Aggregate(exclude_methods=excluded_sources) aggregate_search = aggregate.Aggregate(exclude_methods=excluded_sources)
plain_lyrics: bool = not data.lrc plain_lyrics: bool = not data.lrc
result: Optional[LyricsResult] = await aggregate_search.search(search_artist, search_song, plain_lyrics) result: Optional[LyricsResult|dict] = await aggregate_search.search(search_artist, search_song, plain_lyrics)
if not result: if not result:
return { return {
@ -147,15 +153,15 @@ class LyricSearch(FastAPI):
'errorText': 'Sources exhausted, lyrics not located.', 'errorText': 'Sources exhausted, lyrics not located.',
} }
result: dict = result.todict() result = vars(result)
if data.sub and not data.lrc: if data.sub and not data.lrc:
seeked_found_line: Optional[int] = None 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): for i, line in enumerate(lyric_lines):
line: str = regex.sub(r'\u2064', '', line.strip()) line = regex.sub(r'\u2064', '', line.strip())
if data.sub.strip().lower() in line.strip().lower(): if data.sub.strip().lower() in line.strip().lower():
seeked_found_line: int = i seeked_found_line = i
logging.debug("Found %s at %s, match for %s!", logging.debug("Found %s at %s, match for %s!",
line, seeked_found_line, data.sub) # REMOVEME: DEBUG line, seeked_found_line, data.sub) # REMOVEME: DEBUG
break break

View File

@ -9,11 +9,10 @@ from lyric_search.sources import private, cache as LyricsCache, redis_cache
class Misc(FastAPI): class Misc(FastAPI):
"""Misc Endpoints""" """Misc Endpoints"""
def __init__(self, app: FastAPI, my_util, constants, glob_state, radio): # pylint: disable=super-init-not-called def __init__(self, app: FastAPI, my_util, constants, radio): # pylint: disable=super-init-not-called
self.app = app self.app = app
self.util = my_util self.util = my_util
self.constants = constants self.constants = constants
self.glob_state = glob_state
self.radio_pubkey: str = "XC-AJCJS89-AOLOFKZ92921AK-AKASKZJAN178-3D1" self.radio_pubkey: str = "XC-AJCJS89-AOLOFKZ92921AK-AKASKZJAN178-3D1"
self.lyr_cache = LyricsCache.Cache() self.lyr_cache = LyricsCache.Cache()
self.redis_cache = redis_cache.RedisCache() self.redis_cache = redis_cache.RedisCache()

View File

@ -28,12 +28,11 @@ TODO:
class Radio(FastAPI): class Radio(FastAPI):
"""Radio Endpoints""" """Radio Endpoints"""
def __init__(self, app: FastAPI, my_util, constants, glob_state) -> None: # pylint: disable=super-init-not-called def __init__(self, app: FastAPI, my_util, constants) -> None: # pylint: disable=super-init-not-called
self.app = app self.app = app
self.util = my_util self.util = my_util
self.constants = constants self.constants = constants
self.radio_util = radio_util.RadioUtil(self.constants) self.radio_util = radio_util.RadioUtil(self.constants)
self.glob_state = glob_state
self.endpoints: dict = { self.endpoints: dict = {
"radio/np": self.radio_now_playing, "radio/np": self.radio_now_playing,
@ -64,8 +63,10 @@ class Radio(FastAPI):
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key): if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized") raise HTTPException(status_code=403, detail="Unauthorized")
if data.skipTo: if data.skipTo:
(x, _) = self.radio_util.get_queue_item_by_uuid(data.skipTo) queue_item = self.radio_util.get_queue_item_by_uuid(data.skipTo)
self.radio_util.active_playlist = self.radio_util.active_playlist[x:] if not queue_item:
return False
self.radio_util.active_playlist = self.radio_util.active_playlist[queue_item[0]:]
if not self.radio_util.active_playlist: if not self.radio_util.active_playlist:
await self.radio_util.load_playlist() await self.radio_util.load_playlist()
return await self.radio_util._ls_skip() return await self.radio_util._ls_skip()
@ -112,7 +113,13 @@ class Radio(FastAPI):
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key): if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized") raise HTTPException(status_code=403, detail="Unauthorized")
(x, item) = self.radio_util.get_queue_item_by_uuid(data.uuid) queue_item = self.radio_util.get_queue_item_by_uuid(data.uuid)
if not queue_item:
return {
'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) self.radio_util.active_playlist.insert(0, item)
if not data.next: if not data.next:
@ -126,18 +133,18 @@ class Radio(FastAPI):
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key): if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized") raise HTTPException(status_code=403, detail="Unauthorized")
(x, found_item) = self.radio_util.get_queue_item_by_uuid(data.uuid) queue_item = self.radio_util.get_queue_item_by_uuid(data.uuid)
if not found_item: if not queue_item:
return { return {
'ok': False, 'err': True,
'err': 'UUID not found in play queue', 'errorText': 'Queue item not found.',
} }
self.radio_util.active_playlist.pop(x) self.radio_util.active_playlist.pop(queue_item[0])
return { return {
'ok': True, 'ok': True,
} }
async def album_art_handler(self, request: Request, track_id: Optional[int] = None) -> bytes: async def album_art_handler(self, request: Request, track_id: Optional[int] = None) -> Response:
""" """
Get album art, optional parameter track_id may be specified. Get album art, optional parameter track_id may be specified.
Otherwise, current track album art will be pulled. Otherwise, current track album art will be pulled.
@ -182,13 +189,13 @@ class Radio(FastAPI):
if not isinstance(self.radio_util.active_playlist, list) or not self.radio_util.active_playlist: 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.load_playlist()
await self.radio_util._ls_skip() await self.radio_util._ls_skip()
return return None
next = self.radio_util.active_playlist.pop(0) next = self.radio_util.active_playlist.pop(0)
if not isinstance(next, dict): if not isinstance(next, dict):
logging.critical("next is of type: %s, reloading playlist...", type(next)) logging.critical("next is of type: %s, reloading playlist...", type(next))
await self.radio_util.load_playlist() await self.radio_util.load_playlist()
await self.radio_util._ls_skip() await self.radio_util._ls_skip()
return return None
duration: int = next['duration'] duration: int = next['duration']
time_started: int = int(time.time()) time_started: int = int(time.time())
@ -209,19 +216,21 @@ class Radio(FastAPI):
try: try:
if not 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']) album_art = await self.radio_util.get_album_art(file_path=next['file_path'])
if not album_art:
return None
await self.radio_util.cache_album_art(next['id'], album_art) await self.radio_util.cache_album_art(next['id'], album_art)
except: except:
traceback.print_exc() traceback.print_exc()
return next return next
async def radio_request(self, data: ValidRadioSongRequest, request: Request) -> Response: async def radio_request(self, data: ValidRadioSongRequest, request: Request) -> dict:
"""Song request handler""" """Song request handler"""
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key): if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized") raise HTTPException(status_code=403, detail="Unauthorized")
artistsong: str = data.artistsong artistsong: Optional[str] = data.artistsong
artist: str = data.artist artist: Optional[str] = data.artist
song: str = data.song song: Optional[str] = data.song
if artistsong and (artist or song): if artistsong and (artist or song):
return { return {
'err': True, 'err': True,

View File

@ -13,7 +13,7 @@ import os
import gpt import gpt
from aiohttp import ClientSession, ClientTimeout from aiohttp import ClientSession, ClientTimeout
import aiosqlite as sqlite3 import aiosqlite as sqlite3
from typing import Optional, LiteralString from typing import Union, Optional, LiteralString
from uuid import uuid4 as uuid from uuid import uuid4 as uuid
from .constructors import RadioException from .constructors import RadioException
@ -28,7 +28,7 @@ class RadioUtil:
self.active_playlist_path: str|LiteralString = os.path.join("/usr/local/share", self.active_playlist_path: str|LiteralString = os.path.join("/usr/local/share",
"sqlite_dbs", "track_file_map.db") "sqlite_dbs", "track_file_map.db")
self.active_playlist_name = "default" # not used self.active_playlist_name = "default" # not used
self.active_playlist: list = [] self.active_playlist: list[dict] = []
self.now_playing: dict = { self.now_playing: dict = {
'artist': 'N/A', 'artist': 'N/A',
'song': 'N/A', 'song': 'N/A',
@ -75,8 +75,6 @@ class RadioUtil:
if not artistsong and (not artist or not song): if not artistsong and (not artist or not song):
raise RadioException("No query provided") raise RadioException("No query provided")
try: try:
search_artist: Optional[str] = None
search_song: Optional[str] = None
search_query: str = 'SELECT id, artist, song, (artist || " - " || song) AS artistsong, genre, file_path, duration FROM tracks\ search_query: str = 'SELECT id, artist, song, (artist || " - " || song) AS artistsong, genre, file_path, duration FROM tracks\
WHERE editdist3((lower(artist) || " " || lower(song)), (? || " " || ?))\ WHERE editdist3((lower(artist) || " " || lower(song)), (? || " " || ?))\
<= 410 ORDER BY editdist3((lower(artist) || " " || lower(song)), ?) ASC LIMIT 1' <= 410 ORDER BY editdist3((lower(artist) || " " || lower(song)), ?) ASC LIMIT 1'
@ -84,10 +82,10 @@ class RadioUtil:
artistsong_split: list = artistsong.split(" - ", maxsplit=1) artistsong_split: list = artistsong.split(" - ", maxsplit=1)
(search_artist, search_song) = tuple(artistsong_split) (search_artist, search_song) = tuple(artistsong_split)
else: else:
search_artist: str = artist search_artist = artist
search_song: str = song search_song = song
if not artistsong: if not artistsong:
artistsong: str = f"{search_artist} - {search_song}" artistsong = f"{search_artist} - {search_song}"
search_params = (search_artist.lower(), search_song.lower(), artistsong.lower(),) search_params = (search_artist.lower(), search_song.lower(), artistsong.lower(),)
async with sqlite3.connect(self.active_playlist_path, async with sqlite3.connect(self.active_playlist_path,
timeout=2) as db_conn: timeout=2) as db_conn:
@ -97,7 +95,7 @@ class RadioUtil:
db_conn.row_factory = sqlite3.Row db_conn.row_factory = sqlite3.Row
async with await db_conn.execute(search_query, search_params) as db_cursor: async with await db_conn.execute(search_query, search_params) as db_cursor:
result: Optional[sqlite3.Row|bool] = await db_cursor.fetchone() result: Optional[sqlite3.Row|bool] = await db_cursor.fetchone()
if not result: if not result or not isinstance(result, sqlite3.Row):
return False return False
pushObj: dict = { pushObj: dict = {
'id': result['id'], 'id': result['id'],
@ -116,7 +114,7 @@ class RadioUtil:
traceback.print_exc() traceback.print_exc()
return False return False
async def load_playlist(self): async def load_playlist(self) -> None:
"""Load Playlist""" """Load Playlist"""
try: try:
logging.info(f"Loading playlist...") logging.info(f"Loading playlist...")
@ -135,12 +133,19 @@ class RadioUtil:
"grunge", "house", "dubstep", "hardcore", "hair metal", "horror punk", "folk punk", "breakcore",\ "grunge", "house", "dubstep", "hardcore", "hair metal", "horror punk", "folk punk", "breakcore",\
"post-rock", "deathcore", "hardcore punk", "synthwave", "trap") GROUP BY artistdashsong ORDER BY RANDOM()' "post-rock", "deathcore", "hardcore punk", "synthwave", "trap") GROUP BY artistdashsong ORDER BY RANDOM()'
"""
LIMITED TO ONE ARTIST...
"""
# db_query: str = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, genre, file_path, duration FROM tracks\
# WHERE artist LIKE "%bad omens%" GROUP BY artistdashsong ORDER BY RANDOM()'
async with sqlite3.connect(self.active_playlist_path, async with sqlite3.connect(self.active_playlist_path,
timeout=2) as db_conn: timeout=2) as db_conn:
db_conn.row_factory = sqlite3.Row db_conn.row_factory = sqlite3.Row
async with await db_conn.execute(db_query) as db_cursor: async with await db_conn.execute(db_query) as db_cursor:
results: Optional[list[sqlite3.Row]] = await db_cursor.fetchall() results: list[sqlite3.Row] = await db_cursor.fetchall()
self.active_playlist: list[dict] = [{ self.active_playlist = [{
'uuid': str(uuid().hex), 'uuid': str(uuid().hex),
'id': r['id'], 'id': r['id'],
'artist': double_space.sub(' ', r['artist']).strip(), 'artist': double_space.sub(' ', r['artist']).strip(),
@ -174,7 +179,7 @@ class RadioUtil:
traceback.print_exc() traceback.print_exc()
async def get_album_art(self, track_id: Optional[int] = None, async def get_album_art(self, track_id: Optional[int] = None,
file_path: Optional[str] = None) -> bytes: file_path: Optional[str] = None) -> Optional[bytes]:
""" """
Get Album Art Get Album Art
Args: Args:
@ -191,18 +196,18 @@ class RadioUtil:
query_params: tuple = (track_id,) query_params: tuple = (track_id,)
if file_path and not track_id: if file_path and not track_id:
query: str = "SELECT album_art FROM tracks WHERE file_path = ?" query = "SELECT album_art FROM tracks WHERE file_path = ?"
query_params: tuple = (file_path,) query_params = (file_path,)
async with await db_conn.execute(query, async with await db_conn.execute(query,
query_params) as db_cursor: query_params) as db_cursor:
result: Optional[sqlite3.Row|bool] = await db_cursor.fetchone() result: Optional[sqlite3.Row|bool] = await db_cursor.fetchone()
if not result: if not result or not isinstance(result, sqlite3.Row):
return return None
return result['album_art'] return result['album_art']
except: except:
traceback.print_exc() traceback.print_exc()
return return None
def get_queue_item_by_uuid(self, uuid: str) -> Optional[tuple[int, dict]]: def get_queue_item_by_uuid(self, uuid: str) -> Optional[tuple[int, dict]]:
""" """
@ -249,7 +254,7 @@ class RadioUtil:
response: Optional[str] = await self.gpt.get_completion(prompt=f"I am going to listen to {song} by {artist}.") response: Optional[str] = await self.gpt.get_completion(prompt=f"I am going to listen to {song} by {artist}.")
if not response: if not response:
logging.critical("No response received from GPT?") logging.critical("No response received from GPT?")
return return None
return response return response
async def webhook_song_change(self, track: dict) -> None: async def webhook_song_change(self, track: dict) -> None:
@ -314,7 +319,7 @@ class RadioUtil:
if not ai_response: if not ai_response:
return return
hook_data: dict = { hook_data = {
'username': 'GPT', 'username': 'GPT',
"embeds": [{ "embeds": [{
"title": "AI Feedback", "title": "AI Feedback",

View File

@ -2,29 +2,28 @@
import os import os
import random import random
from typing import LiteralString from typing import LiteralString, Optional
import aiosqlite as sqlite3 import aiosqlite as sqlite3
from fastapi import FastAPI from fastapi import FastAPI
from .constructors import RandMsgRequest from .constructors import RandMsgRequest
class RandMsg(FastAPI): class RandMsg(FastAPI):
"""Random Message Endpoint""" """Random Message Endpoint"""
def __init__(self, app: FastAPI, util, constants, glob_state): # pylint: disable=super-init-not-called def __init__(self, app: FastAPI, util, constants): # pylint: disable=super-init-not-called
self.app = app self.app = app
self.util = util self.util = util
self.constants = constants self.constants = constants
self.glob_state = glob_state
self.endpoint_name = "randmsg" self.endpoint_name = "randmsg"
app.add_api_route(f"/{self.endpoint_name}", self.randmsg_handler, methods=["POST"]) app.add_api_route(f"/{self.endpoint_name}", self.randmsg_handler, methods=["POST"])
async def randmsg_handler(self, data: RandMsgRequest = None): async def randmsg_handler(self, data: RandMsgRequest):
""" """
Get a randomly generated message Get a randomly generated message
""" """
random.seed() random.seed()
short: bool = data.short if data else False short: bool = data.short if data.short else False
if not short: if not short:
db_rand_selected = random.choice([0, 1, 3]) db_rand_selected = random.choice([0, 1, 3])
else: else:
@ -33,15 +32,13 @@ class RandMsg(FastAPI):
match db_rand_selected: match db_rand_selected:
case 0: case 0:
randmsg_db_path = os.path.join("/", randmsg_db_path = os.path.join("/usr/local/share",
"usr", "local", "share",
"sqlite_dbs", "qajoke.db") # For qajoke db "sqlite_dbs", "qajoke.db") # For qajoke db
db_query = "SELECT id, ('<b>Q:</b> ' || question || '<br/><b>A:</b> ' \ db_query = "SELECT id, ('<b>Q:</b> ' || question || '<br/><b>A:</b> ' \
|| answer) FROM jokes ORDER BY RANDOM() LIMIT 1" # For qajoke db || answer) FROM jokes ORDER BY RANDOM() LIMIT 1" # For qajoke db
title_attr = "QA Joke DB" title_attr = "QA Joke DB"
case 1 | 9: case 1 | 9:
randmsg_db_path = os.path.join("/", randmsg_db_path = os.path.join("/usr/local/share",
"usr", "local", "share",
"sqlite_dbs", "sqlite_dbs",
"randmsg.db") # For randmsg db "randmsg.db") # For randmsg db
db_query = "SELECT id, msg FROM msgs WHERE \ db_query = "SELECT id, msg FROM msgs WHERE \
@ -50,46 +47,41 @@ class RandMsg(FastAPI):
db_query = db_query.replace("<= 180", "<= 126") db_query = db_query.replace("<= 180", "<= 126")
title_attr = "Random Msg DB" title_attr = "Random Msg DB"
case 2: case 2:
randmsg_db_path = os.path.join("/", randmsg_db_path = os.path.join("/usr/local/share",
"usr", "local", "share",
"sqlite_dbs", "sqlite_dbs",
"trump.db") # For Trump Tweet DB "trump.db") # For Trump Tweet DB
db_query = "SELECT id, content FROM tweets \ 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" title_attr = "Trump Tweet DB"
case 3: case 3:
randmsg_db_path: str|LiteralString = os.path.join("/", randmsg_db_path = os.path.join("/usr/local/share",
"usr", "local", "share",
"sqlite_dbs", "sqlite_dbs",
"philo.db") # For Philo DB "philo.db") # For Philo DB
db_query: str = "SELECT id, (content || '<br> - ' || speaker) FROM quotes \ db_query = "SELECT id, (content || '<br> - ' || speaker) FROM quotes \
ORDER BY RANDOM() LIMIT 1" # For Philo DB ORDER BY RANDOM() LIMIT 1" # For Philo DB
title_attr: str = "Philosophical Quotes DB" title_attr = "Philosophical Quotes DB"
case 4: case 4:
randmsg_db_path: str|LiteralString = os.path.join("/", randmsg_db_path = os.path.join("/usr/local/share",
"usr", "local", "share",
"sqlite_dbs", "sqlite_dbs",
"hate.db") # For Hate DB "hate.db") # For Hate DB
db_query: str = """SELECT id, ("<font color='#FF0000'>" || comment) FROM hate_speech \ db_query = """SELECT id, ("<font color='#FF0000'>" || comment) FROM hate_speech \
WHERE length(comment) <= 180 ORDER BY RANDOM() LIMIT 1""" WHERE length(comment) <= 180 ORDER BY RANDOM() LIMIT 1"""
title_attr: str = "Hate Speech DB" title_attr = "Hate Speech DB"
case 5: case 5:
randmsg_db_path: str|LiteralString = os.path.join("/", randmsg_db_path = os.path.join("/usr/local/share",
"usr", "local", "share",
"sqlite_dbs", "sqlite_dbs",
"rjokes.db") # r/jokes DB "rjokes.db") # r/jokes DB
db_query: str = """SELECT id, (title || "<br>" || body) FROM jokes \ db_query = """SELECT id, (title || "<br>" || body) FROM jokes \
WHERE score >= 10000 ORDER BY RANDOM() LIMIT 1""" WHERE score >= 10000 ORDER BY RANDOM() LIMIT 1"""
title_attr: str = "r/jokes DB" title_attr = "r/jokes DB"
case 6: case 6:
randmsg_db_path: str|LiteralString = os.path.join("/", randmsg_db_path = os.path.join("/usr/local/share",
"usr", "local", "share",
"sqlite_dbs", "sqlite_dbs",
"donnies.db") # Donnies DB "donnies.db") # Donnies DB
random.seed() random.seed()
twilight_or_mice: str = random.choice(["twilight", "mice"]) twilight_or_mice: str = random.choice(["twilight", "mice"])
db_query: str = f"SELECT id, text FROM {twilight_or_mice} ORDER BY RANDOM() LIMIT 1" db_query = f"SELECT id, text FROM {twilight_or_mice} ORDER BY RANDOM() LIMIT 1"
title_attr: str = "Donnies DB" title_attr = "Donnies DB"
async with sqlite3.connect(database=randmsg_db_path, timeout=1) as _db: async with sqlite3.connect(database=randmsg_db_path, timeout=1) as _db:
async with await _db.execute(db_query) as _cursor: async with await _db.execute(db_query) as _cursor:

View File

@ -8,11 +8,10 @@ from .constructors import ValidShowEpisodeLineRequest, ValidShowEpisodeListReque
class Transcriptions(FastAPI): class Transcriptions(FastAPI):
"""Transcription Endpoints""" """Transcription Endpoints"""
def __init__(self, app: FastAPI, util, constants, glob_state) -> None: # pylint: disable=super-init-not-called def __init__(self, app: FastAPI, util, constants) -> None: # pylint: disable=super-init-not-called
self.app = app self.app = app
self.util = util self.util = util
self.constants = constants self.constants = constants
self.glob_state = glob_state
self.endpoints: dict = { self.endpoints: dict = {
"transcriptions/get_episodes": self.get_episodes_handler, "transcriptions/get_episodes": self.get_episodes_handler,
@ -34,39 +33,39 @@ class Transcriptions(FastAPI):
if show_id is None: if show_id is None:
return { return {
'err': True, 'err': True,
'errorText': 'Invalid request' 'errorText': 'Invalid request',
} }
show_id: int = int(show_id) show_id = int(show_id)
if not(str(show_id).isnumeric()) or show_id not in [0, 1, 2]: if not(str(show_id).isnumeric()) or show_id not in [0, 1, 2]:
return { return {
'err': True, 'err': True,
'errorText': 'Show not found.' 'errorText': 'Show not found.',
} }
match show_id: match show_id:
case 0: case 0:
db_path: str|LiteralString = os.path.join("/", "usr", "local", "share", db_path = os.path.join("/usr/local/share",
"sqlite_dbs", "sp.db") "sqlite_dbs", "sp.db")
db_query: str = """SELECT DISTINCT(("S" || Season || "E" || Episode || " " || Title)), ID FROM SP_DAT ORDER BY Season, Episode""" db_query = """SELECT DISTINCT(("S" || Season || "E" || Episode || " " || Title)), ID FROM SP_DAT ORDER BY Season, Episode"""
show_title: str = "South Park" show_title = "South Park"
case 1: case 1:
db_path: str|LiteralString = os.path.join("/", "usr", "local", "share", db_path = os.path.join("/usr/local/share",
"sqlite_dbs", "futur.db") "sqlite_dbs", "futur.db")
db_query: str = """SELECT DISTINCT(("S" || EP_S || "E" || EP_EP || " " || EP_TITLE)), EP_ID FROM clean_dialog ORDER BY EP_S, EP_EP""" 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: str = "Futurama" show_title = "Futurama"
case 2: case 2:
db_path: str|LiteralString = os.path.join("/", "usr", "local", "share", db_path = os.path.join("/usr/local/share",
"sqlite_dbs", "parks.db") "sqlite_dbs", "parks.db")
db_query: str = """SELECT DISTINCT(("S" || EP_S || "E" || EP_EP || " " || EP_TITLE)), EP_ID FROM clean_dialog ORDER BY EP_S, EP_EP""" 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: str = "Parks And Rec" show_title = "Parks And Rec"
case _: case _:
return { return {
'err': True, 'err': True,
'errorText': 'Unknown error.' 'errorText': 'Unknown error.'
} }
await self.glob_state.increment_counter('transcript_list_requests')
async with sqlite3.connect(database=db_path, timeout=1) as _db: async with sqlite3.connect(database=db_path, timeout=1) as _db:
async with await _db.execute(db_query) as _cursor: async with await _db.execute(db_query) as _cursor:
result: list[tuple] = await _cursor.fetchall() result: list[tuple] = await _cursor.fetchall()
@ -86,17 +85,17 @@ class Transcriptions(FastAPI):
# pylint: disable=line-too-long # pylint: disable=line-too-long
match show_id: match show_id:
case 0: case 0:
db_path: str|LiteralString = os.path.join("/", "usr", "local", "share", db_path = os.path.join("/usr/local/share",
"sqlite_dbs", "sp.db") "sqlite_dbs", "sp.db")
db_query: str = """SELECT ("S" || Season || "E" || Episode || " " || Title), Character, Line FROM SP_DAT WHERE ID = ?""" db_query = """SELECT ("S" || Season || "E" || Episode || " " || Title), Character, Line FROM SP_DAT WHERE ID = ?"""
case 1: case 1:
db_path: str|LiteralString = os.path.join("/", "usr", "local", "share", db_path = os.path.join("/usr/local/share",
"sqlite_dbs", "futur.db") "sqlite_dbs", "futur.db")
db_query: str = """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""" 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: case 2:
db_path: str|LiteralString = os.path.join("/", "usr", "local", "share", db_path = os.path.join("/usr/local/share",
"sqlite_dbs", "parks.db") "sqlite_dbs", "parks.db")
db_query: str = """SELECT ("S" || EP_S || "E" || EP_EP || " " || EP_TITLE), EP_LINE_SPEAKER, EP_LINE FROM clean_dialog WHERE EP_ID = ? ORDER BY id ASC""" 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 _: case _:
return { return {
@ -104,7 +103,6 @@ class Transcriptions(FastAPI):
'errorText': 'Unknown error' 'errorText': 'Unknown error'
} }
await self.glob_state.increment_counter('transcript_requests')
async with sqlite3.connect(database=db_path, timeout=1) as _db: async with sqlite3.connect(database=db_path, timeout=1) as _db:
params: tuple = (episode_id,) params: tuple = (episode_id,)
async with await _db.execute(db_query, params) as _cursor: async with await _db.execute(db_query, params) as _cursor:

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3.12 #!/usr/bin/env python3.12
import logging import logging
from typing import Optional
from fastapi import FastAPI, Request, HTTPException from fastapi import FastAPI, Request, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
from aiohttp import ClientSession, ClientTimeout from aiohttp import ClientSession, ClientTimeout
@ -9,12 +10,10 @@ from .constructors import ValidXCRequest
class XC(FastAPI): class XC(FastAPI):
"""XC (CrossComm) Endpoints""" """XC (CrossComm) Endpoints"""
def __init__(self, app: FastAPI, util, constants, glob_state) -> None: # pylint: disable=super-init-not-called def __init__(self, app: FastAPI, util, constants) -> None: # pylint: disable=super-init-not-called
self.app = app self.app = app
self.util = util self.util = util
self.constants = constants self.constants = constants
self.glob_state = glob_state
self.endpoints: dict = { self.endpoints: dict = {
"xc": self.xc_handler, "xc": self.xc_handler,
@ -31,7 +30,7 @@ class XC(FastAPI):
key: str = data.key key: str = data.key
bid: int = data.bid bid: int = data.bid
cmd: str = data.cmd cmd: str = data.cmd
cmd_data: dict = data.data cmd_data: Optional[dict] = data.data
if not self.util.check_key(path=request.url.path, req_type=0, key=key): if not self.util.check_key(path=request.url.path, req_type=0, key=key):
raise HTTPException(status_code=403, detail="Unauthorized") raise HTTPException(status_code=403, detail="Unauthorized")
@ -50,11 +49,15 @@ class XC(FastAPI):
async with ClientSession() as session: async with ClientSession() as session:
async with await session.post(f"{bot_api_url}{cmd}", json=cmd_data, headers={ async with await session.post(f"{bot_api_url}{cmd}", json=cmd_data, headers={
'Content-Type': 'application/json; charset=utf-8' 'Content-Type': 'application/json; charset=utf-8'
}, timeout=ClientTimeout(connect=5, sock_read=5)) as request: }, timeout=ClientTimeout(connect=5, sock_read=5)) as aiohttp_request:
response: dict = await request.json() response: dict = await aiohttp_request.json()
return { return {
'success': True, 'success': True,
'response': response 'response': response
} }
except Exception as e: except Exception as e:
logging.debug("Error: %s", str(e)) logging.debug("Error: %s", str(e))
return {
'err': True,
'errorText': 'General error.',
}

View File

@ -8,11 +8,10 @@ from .constructors import ValidYTSearchRequest
class YT(FastAPI): class YT(FastAPI):
"""YT Endpoints""" """YT Endpoints"""
def __init__(self, app: FastAPI, util, constants, glob_state) -> None: # pylint: disable=super-init-not-called def __init__(self, app: FastAPI, util, constants) -> None: # pylint: disable=super-init-not-called
self.app = app self.app = app
self.util = util self.util = util
self.constants = constants self.constants = constants
self.glob_state = glob_state
self.ytsearch = importlib.import_module("youtube_search_async").YoutubeSearch() self.ytsearch = importlib.import_module("youtube_search_async").YoutubeSearch()
self.endpoints: dict = { self.endpoints: dict = {

View File

@ -12,7 +12,7 @@ class GPT:
) )
self.default_system_prompt = "You are a helpful assistant who will provide only totally accurate tidbits of info on the specific songs the user may listen to." self.default_system_prompt = "You are a helpful assistant who will provide only totally accurate tidbits of info on the specific songs the user may listen to."
async def get_completion(self, prompt: str, system_prompt: Optional[str] = None) -> None: async def get_completion(self, prompt: str, system_prompt: Optional[str] = None) -> str:
if not system_prompt: if not system_prompt:
system_prompt = self.default_system_prompt system_prompt = self.default_system_prompt
chat_completion = await self.client.chat.completions.create( chat_completion = await self.client.chat.completions.create(

View File

@ -3,7 +3,7 @@
import traceback import traceback
import logging import logging
from typing import Optional from typing import Optional, Union
import regex import regex
from aiohttp import ClientSession, ClientTimeout from aiohttp import ClientSession, ClientTimeout
from constants import Constants from constants import Constants
@ -30,13 +30,13 @@ class LastFM:
timeout=ClientTimeout(connect=3, sock_read=8)) as request: timeout=ClientTimeout(connect=3, sock_read=8)) as request:
request.raise_for_status() request.raise_for_status()
data: dict = await request.json() data: dict = await request.json()
data = data.get('artist') data = data.get('artist', 'N/A')
ret_obj: dict = { ret_obj: dict = {
'id': data.get('mbid'), 'id': data.get('mbid'),
'touring': data.get('ontour'), 'touring': data.get('ontour'),
'name': data.get('name'), 'name': data.get('name'),
'bio': data.get('bio').get('summary').strip()\ 'bio': data.get('bio', None).get('summary').strip()\
.split("<a href")[0], .split("<a href")[0],
} }
return ret_obj return ret_obj
@ -68,10 +68,10 @@ class LastFM:
timeout=ClientTimeout(connect=3, sock_read=8)) as request: timeout=ClientTimeout(connect=3, sock_read=8)) as request:
request.raise_for_status() request.raise_for_status()
data: dict = await request.json() data: dict = await request.json()
data = data.get('track') data = data.get('track', None)
ret_obj: dict = { ret_obj: dict = {
'artist_mbid': data.get('artist').get('mbid'), 'artist_mbid': data.get('artist', None).get('mbid'),
'album': data.get('album').get('title'), 'album': data.get('album', None).get('title'),
} }
return ret_obj return ret_obj
except: except:
@ -97,8 +97,8 @@ class LastFM:
'err': 'No artist or album specified', 'err': 'No artist or album specified',
} }
tracks: list|dict = await self.get_release(artist=artist, album=album) tracks: dict = await self.get_release(artist=artist, album=album)
tracks: list|dict = tracks.get('tracks') tracks = tracks.get('tracks', None)
ret_obj: dict = { ret_obj: dict = {
'tracks': tracks, 'tracks': tracks,
} }
@ -129,9 +129,9 @@ class LastFM:
async with await session.get(f"{self.api_base_url}artist.gettopalbums&artist={artist}&api_key={self.creds.get('key')}&autocorrect=1&format=json", async with await session.get(f"{self.api_base_url}artist.gettopalbums&artist={artist}&api_key={self.creds.get('key')}&autocorrect=1&format=json",
timeout=ClientTimeout(connect=3, sock_read=8)) as request: timeout=ClientTimeout(connect=3, sock_read=8)) as request:
request.raise_for_status() request.raise_for_status()
data: dict = await request.json() json_data: dict = await request.json()
data: str = data.get('topalbums').get('album') data: dict = data.get('topalbums', None).get('album')
ret_obj: dict = [ ret_obj: list = [
{ {
'title': item.get('name') 'title': item.get('name')
} for item in data if not(item.get('name').lower() == "(null)") and int(item.get('playcount')) >= 50 } for item in data if not(item.get('name').lower() == "(null)") and int(item.get('playcount')) >= 50
@ -143,7 +143,7 @@ class LastFM:
'err': 'Failed', 'err': 'Failed',
} }
async def get_artist_id(self, artist: Optional[str] = None) -> int|dict: async def get_artist_id(self, artist: Optional[str] = None) -> int:
""" """
Get Artist ID from LastFM Get Artist ID from LastFM
Args: Args:
@ -153,22 +153,16 @@ class LastFM:
""" """
try: try:
if artist is None: if artist is None:
return { return -1
'err': 'No artist specified.',
}
artist_search: dict = await self.search_artist(artist=artist) artist_search: dict = await self.search_artist(artist=artist)
if not artist_search: if not artist_search:
logging.debug("[get_artist_id] Throwing no result error") logging.debug("[get_artist_id] Throwing no result error")
return { return -1
'err': 'No results.',
}
artist_id: int = int(artist_search[0].get('id', 0)) artist_id: int = int(artist_search[0].get('id', 0))
return artist_id return artist_id
except: except:
traceback.print_exc() traceback.print_exc()
return { return -1
'err': 'Failed',
}
async def get_artist_info_by_id(self, artist_id: Optional[int] = None) -> dict: async def get_artist_info_by_id(self, artist_id: Optional[int] = None) -> dict:
""" """
@ -219,12 +213,12 @@ class LastFM:
return { return {
'err': 'No artist specified.', 'err': 'No artist specified.',
} }
artist_id: int = await self.get_artist_id(artist=artist) artist_id: Optional[int] = await self.get_artist_id(artist=artist)
if not artist_id: if not artist_id:
return { return {
'err': 'Failed', 'err': 'Failed',
} }
artist_info: dict = await self.get_artist_info_by_id(artist_id=artist_id) artist_info: Optional[dict] = await self.get_artist_info_by_id(artist_id=artist_id)
if not artist_info: if not artist_info:
return { return {
'err': 'Failed', 'err': 'Failed',
@ -258,21 +252,21 @@ class LastFM:
async with await session.get(req_url, async with await session.get(req_url,
timeout=ClientTimeout(connect=3, sock_read=8)) as request: timeout=ClientTimeout(connect=3, sock_read=8)) as request:
request.raise_for_status() request.raise_for_status()
data: dict = await request.json() json_data: dict = await request.json()
data: dict = data.get('album') data: dict = json_data.get('album', None)
ret_obj: dict = { ret_obj: dict = {
'id': data.get('mbid'), 'id': data.get('mbid'),
'artists': data.get('artist'), 'artists': data.get('artist'),
'tags': data.get('tags'), 'tags': data.get('tags'),
'title': data.get('name'), 'title': data.get('name'),
'summary': data.get('wiki').get('summary').split("<a href")[0]\ 'summary': data.get('wiki', None).get('summary').split("<a href")[0]\
if "wiki" in data.keys()\ if "wiki" in data.keys()\
else "No summary available for this release.", else "No summary available for this release.",
} }
try: try:
track_key: list = data.get('tracks').get('track') track_key: list = data.get('tracks', None).get('track')
except: except:
track_key: list = [] track_key = []
if isinstance(track_key, list): if isinstance(track_key, list):
ret_obj['tracks'] = [ ret_obj['tracks'] = [
{ {

View File

@ -19,7 +19,3 @@ class LyricsResult:
lyrics: str|list lyrics: str|list
confidence: int confidence: int
time: float = 0.00 time: float = 0.00
def todict(self) -> dict:
"""Return as dict"""
return asdict(self)

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3.12 #!/usr/bin/env python3.12
SCRAPE_HEADERS = { SCRAPE_HEADERS: dict[str, str] = {
'accept': '*/*', 'accept': '*/*',
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0', 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0',
} }

View File

@ -14,7 +14,6 @@ from . import private, common, cache, redis_cache
from lyric_search import utils from lyric_search import utils
from lyric_search.constructors import LyricsResult from lyric_search.constructors import LyricsResult
logger = logging.getLogger() logger = logging.getLogger()
log_level = logging.getLevelName(logger.level) log_level = logging.getLevelName(logger.level)

View File

@ -23,3 +23,5 @@ dependencies = [
"typing-inspect>=0.9.0", "typing-inspect>=0.9.0",
"http3>=0.6.7", "http3>=0.6.7",
] ]
[tool.mypy]
disable_error_code = ["import-untyped"]

111
state.py
View File

@ -1,111 +0,0 @@
#!/usr/bin/env python3.12
# pylint: disable=bare-except, broad-exception-raised
"""Global State Storage/Counters"""
import logging
import os
import aiosqlite as sqlite3
from fastapi import FastAPI
from fastapi_utils.tasks import repeat_every
class State(FastAPI):
"""Global State for API"""
def __init__(self, app: FastAPI, util, constants): # pylint: disable=unused-argument
super().__init__()
self.counter_db_path = os.path.join("/", "usr", "local", "share",
"sqlite_dbs", "stats.db")
self.counters = {
str(counter): 0 for counter in constants.AVAILABLE_COUNTERS
}
self.counters_initialized = False
logging.debug("[State] Counters: %s", self.counters)
@app.on_event("startup")
async def get_counters():
logging.info("[State] Initializing counters...")
async with sqlite3.connect(self.counter_db_path, timeout=2) as _db:
_query = "SELECT ai_requests, lyric_requests, transcript_list_requests, transcript_requests, lyrichistory_requests, \
failedlyric_requests, misc_failures, claude_ai_requests FROM counters LIMIT 1"
await _db.executescript("pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory; pragma mmap_size = 30000000000;")
async with _db.execute(_query) as _cursor:
_result = await _cursor.fetchone()
(ai_requests,
lyric_requests,
transcript_list_requests,
transcript_requests,
lyrichistory_requests,
failedlyric_requests,
misc_failures,
claude_ai_requests) = _result
self.counters = {
'ai_requests': ai_requests,
'lyric_requests': lyric_requests,
'transcript_list_requests': transcript_list_requests,
'transcript_requests': transcript_requests,
'lyrichistory_requests': lyrichistory_requests,
'failedlyric_requests': failedlyric_requests,
'misc_failures': misc_failures,
'claude_ai_requests': claude_ai_requests
}
self.counters_initialized = True
logging.info("Counters loaded from db: %s", self.counters)
@app.on_event("startup")
@repeat_every(seconds=10)
async def update_db():
if not self.counters_initialized:
logging.debug("[State] TICK: Counters not yet initialized")
return
ai_requests = self.counters.get('ai_requests')
lyric_requests = self.counters.get('lyric_requests')
transcript_list_requests = self.counters.get('transcript_list_requests')
transcript_requests = self.counters.get('transcript_requests')
lyrichistory_requests = self.counters.get('lyrichistory_requests')
failedlyric_requests = self.counters.get('failedlyric_requests')
claude_ai_requests = self.counters.get('claude_ai_requests')
async with sqlite3.connect(self.counter_db_path, timeout=2) as _db:
_query = "UPDATE counters SET ai_requests = ?, lyric_requests = ?, transcript_list_requests = ?, \
transcript_requests = ?, lyrichistory_requests = ?, failedlyric_requests = ?, \
claude_ai_requests = ?"
_params = (ai_requests,
lyric_requests,
transcript_list_requests,
transcript_requests,
lyrichistory_requests,
failedlyric_requests,
claude_ai_requests)
await _db.executescript("pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory; pragma mmap_size = 30000000000;")
async with _db.execute(_query, _params) as _cursor:
if _cursor.rowcount != 1:
logging.error("Failed to update DB")
return
await _db.commit()
logging.debug("[State] Updated DB")
async def increment_counter(self, counter: str):
"""Increment Counter"""
if not counter in self.counters.keys():
raise BaseException(f"[State] Counter {counter} does not exist")
self.counters[counter] += 1
return True
async def get_counter(self, counter: str):
"""Get Counter"""
if not counter in self.counters.keys():
raise BaseException(f"[State] Counter {counter} does not exist")
return self.counters[counter]
async def get_all_counters(self):
"""Get All Counters"""
return self.counters