This commit is contained in:
codey 2025-01-11 20:59:10 -05:00
parent 85a0d6bc62
commit 3c57f13557
18 changed files with 464 additions and 365 deletions

View File

@ -1,23 +0,0 @@
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import List
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket):
self.active_connections.append(websocket)
async def disconnect(self, websocket: WebSocket):
if websocket in self.active_connections:
self.active_connections.remove(websocket)
async def broadcast_raw(self, data: bytes):
for connection in self.active_connections:
try:
await connection.send_bytes(data)
except WebSocketDisconnect:
await self.disconnect(connection)
except Exception as e:
print(f"Error sending to client: {e}")

View File

@ -1,75 +0,0 @@
#!/usr/bin/env python3.12
import soundfile as sf
import asyncio
import numpy as np
import time
from aces.connection_manager import ConnectionManager
from fastapi import WebSocket, WebSocketDisconnect
class AudioStreamer:
def __init__(self, sound_file, ws_connection_mgr):
self.sound_file = sound_file
self.audio_file = None
self.format_info = None
self.chunk_size = 16384 # Larger chunk for better quality
self.ws_connection_mgr = ws_connection_mgr
self.broadcast_task = None
self.init_audio_file()
def init_audio_file(self):
self.audio_file = sf.SoundFile(self.sound_file, 'rb')
self.format_info = {
'samplerate': self.audio_file.samplerate,
'channels': self.audio_file.channels,
'format': 'PCM',
'subtype': str(self.audio_file.subtype)
}
async def broadcast_audio(self):
try:
chunk_duration = self.chunk_size / (self.audio_file.samplerate * self.audio_file.channels)
target_interval = chunk_duration / 2 # Send chunks at twice playback rate for smooth buffering
while True:
if not self.ws_connection_mgr.active_connections:
await asyncio.sleep(0.1)
continue
start_time = asyncio.get_event_loop().time()
chunk = self.audio_file.read(self.chunk_size, dtype='float32')
if len(chunk) == 0:
self.audio_file.seek(0)
continue
await self.ws_connection_mgr.broadcast_raw(chunk.tobytes())
# Calculate how long processing took and adjust sleep time
elapsed = asyncio.get_event_loop().time() - start_time
sleep_time = max(0, target_interval - elapsed)
await asyncio.sleep(sleep_time)
except Exception as e:
print(f"Broadcast error: {e}")
async def handle_client(self, ws: WebSocket):
try:
await self.ws_connection_mgr.connect(ws)
await ws.send_json(self.format_info)
if not self.broadcast_task or self.broadcast_task.done():
self.broadcast_task = asyncio.create_task(self.broadcast_audio())
while True:
try:
await ws.receive_text()
except WebSocketDisconnect:
break
except Exception as e:
print(f"Error in handle_client: {e}")
finally:
await self.ws_connection_mgr.disconnect(ws)

12
base.py
View File

@ -7,7 +7,6 @@ import asyncio
from typing import Any
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi_utils.tasks import repeat_every
logger = logging.getLogger()
@ -44,6 +43,8 @@ allow_headers=["*"])
# pylint: disable=missing-function-docstring
"""
Blacklisted routes
"""
@ -53,22 +54,19 @@ def disallow_get():
return util.get_blocked_response()
@app.get("/{any:path}")
def disallow_get_any(var: Any = None):
def disallow_get_any(var: Any = None): # pylint: disable=unused-argument
return util.get_blocked_response()
@app.post("/")
def disallow_base_post():
return util.get_blocked_response()
# @app.limiter.limit("1/minute")
# @app.post("/lyric_cache_list/")
# async def rate_limited():
# return {"error": "Rate limited"}
"""
End Blacklisted Routes
"""
# pylint: enable=missing-function-docstring
"""
Actionable Routes

3
cah_ext/__init__.py Normal file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env python3.12
from . import constructors

63
cah_ext/constructors.py Normal file
View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3.12
class CAHClient:
"""CAH Client Constructor"""
def __init__(self,
resource: str,
platform: str,
csid: str,
connected_at: int,
players: list,
games: list):
self.resource: str = resource
self.platform: str = platform
self.csid: str = csid
self.connected_at: int = connected_at
self.players: list = players
self.games: list = games
def __iter__(self):
return [value for value in self.__dict__.values() if isinstance(value, int) or isinstance(value, float)].__iter__()
class CAHGame:
"""CAH Game Constructor"""
def __init__(self,
_id: str,
rounds: int,
resources: list[dict],
players: list[dict],
created_at: int,
state: int,
started_at: int,
state_changed_at: int,
):
self.id: str = id
self.rounds: int = rounds
self.resources: list[dict] = resources
self.players: list[dict] = players
self.created_at: int = created_at
self.state: int = state
self.started_at: int = started_at
self.state_changed_at: int = state_changed_at
def __iter__(self):
return [value for value in self.__dict__.values() if isinstance(value, int) or isinstance(value, float)].__iter__()
class CAHPlayer:
"""CAH Player Constructor"""
def __init__(self,
_id: str,
current_game: CAHGame,
platform: str,
related_resource: str,
joined_at: str,
handle: str):
self.id = id
self.current_game = current_game
self.platform = platform
self.related_resource = related_resource
self.joined_at = joined_at
self.handle = handle

123
cah_ext/websocket_conn.py Normal file
View File

@ -0,0 +1,123 @@
#!/usr/bin/env python3.12
# pylint: disable=bare-except, broad-exception-caught, invalid-name
import time
import logging
from fastapi import WebSocket
from cah_ext.constructors import CAHClient
class ConnectionManager:
"""WS Connection Manager"""
def __init__(self):
self.active_connections: dict = {}
def get_connection_by_ws(self, websocket: WebSocket) -> WebSocket:
"""Get Connection by WS"""
return self.active_connections.get(websocket)
def get_connection_by_csid(self, csid: str) -> WebSocket:
"""Get Connection by CSID"""
for connection in self.active_connections:
if connection.get('csid') == csid:
return connection
def get_connection_by_resource_label(self, resource: str):
"""Get Connection by Resource Label"""
for connection in self.active_connections:
try:
if connection.get('client').get('resource') == resource:
return connection
except:
continue
async def send_client_and_game_lists(self, state, websocket: WebSocket):
"""Send Client and Game Lists"""
clients = []
for _, client in self.active_connections.items():
logging.debug("Client: %s", client)
_client = client.get('client')
clients.append({
'resource': _client.resource,
'platform': _client.platform,
'connected_at': _client.connected_at,
})
await websocket.send_json({
"event": "client_list",
"ts": int(time.time()),
"data": {
"clients": clients
}
})
await websocket.send_json({
"event": "game_list",
"ts": int(time.time()),
"data":
{
"games": state.get_games()
}
})
async def connect(self, websocket: WebSocket):
"""Process WS Client Connection"""
await websocket.accept()
self.active_connections[websocket] = {
'client': None,
'websocket': websocket,
}
async def handshake_complete(self,
state,
websocket: WebSocket,
csid: str,
handshakedClient: CAHClient):
"""Process Handshake"""
if websocket in self.active_connections:
self.active_connections.pop(websocket)
self.active_connections[websocket] = {
'websocket': websocket,
'csid': csid,
'client': handshakedClient,
}
await self.broadcast({
"event": "client_connected",
"ts": int(time.time()),
"data": {
"connected_resource": handshakedClient.resource,
"connected_platform": handshakedClient.platform,
}
})
await self.send_client_and_game_lists(state,
websocket)
async def disconnect(self, state, websocket: WebSocket, csid: str = None): # pylint: disable=unused-argument
"""Process WS Client Disconnection"""
disconnected = self.get_connection_by_ws(websocket)
disconnected_client = disconnected.get('client')
disconnected_resource = disconnected_client.resource
disconnected_games = [str(game.id) for game in disconnected_client.games]
await self.broadcast({
"event": "client_disconnected",
"ts": int(time.time()),
"data": {
"disconnected_resource": disconnected_resource,
}
})
await state.remove_resource(disconnected_games, disconnected_resource)
self.active_connections.pop(websocket)
async def send(self, message: str, websocket: WebSocket):
"""Send WS Client some data"""
await websocket.send_json(message)
async def broadcast(self, message: str):
"""Broadcast data to all connected WS clients"""
for connection in self.active_connections:
try:
await connection.send_json(message)
except:
continue

View File

@ -1,14 +1,15 @@
#!/usr/bin/env python3.12
# pylint: disable=bare-except, broad-exception-caught, invalid-name
import importlib
import logging
import traceback
import regex
from aiohttp import ClientSession, ClientTimeout
from fastapi import FastAPI, Security, Request, HTTPException
from fastapi.security import APIKeyHeader, APIKeyQuery
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
from pydantic import BaseModel
class ValidAISongRequest(BaseModel):
"""
- **a**: artist
@ -18,6 +19,19 @@ class ValidAISongRequest(BaseModel):
a: str
s: str
class ValidHookSongRequest(BaseModel):
"""
- **a**: artist
- **s**: track title
- **hook**: hook to return
"""
a: str
s: str
hook: str | None = ""
# pylint: enable=bad-indentation
class AI(FastAPI):
"""AI Endpoints"""
def __init__(self, app: FastAPI, my_util, constants, glob_state): # pylint: disable=super-init-not-called
@ -29,12 +43,48 @@ class AI(FastAPI):
self.endpoints = {
"ai/openai": self.ai_openai_handler,
"ai/base": self.ai_handler,
"ai/song": self.ai_song_handler
"ai/song": self.ai_song_handler,
"ai/hook": self.ai_hook_handler,
#tbd
}
for endpoint, handler in self.endpoints.items():
app.add_api_route(f"/{endpoint}/{{any:path}}", handler, methods=["POST"])
app.add_api_route(f"/{endpoint}/openai/", handler, methods=["POST"])
async def respond_via_webhook(self, data: ValidHookSongRequest, originalRequest: Request):
"""Respond via Webhook"""
try:
logging.debug("Request received: %s", data)
data2 = data.copy()
del data2.hook
response = await self.ai_song_handler(data2, originalRequest)
if not response.get('resp'):
logging.critical("NO RESP!")
return
response = response.get('resp')
hook_data = {
'username': 'Claude',
"embeds": [{
"title": "Claude's Feedback",
"description": response,
"footer": {
"text": "Current model: claude-3-haiku-20240307",
}
}]
}
logging.critical("Request: %s", data)
async with ClientSession() as session:
async with session.post(data.hook, json=hook_data,
timeout=ClientTimeout(connect=5, sock_read=5), headers={
'content-type': 'application/json; charset=utf-8',}) as request:
logging.debug("Returned: %s",
await request.json())
await request.raise_for_status()
return True
except:
traceback.print_exc()
return False
async def ai_handler(self, request: Request):
"""
@ -46,10 +96,10 @@ class AI(FastAPI):
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')):
raise HTTPException(status_code=403, detail="Unauthorized")
local_llm_headers = {
'Authorization': f'Bearer {self.constants.LOCAL_LLM_KEY}'
}
forward_path = self.url_clean_regex.sub('', request.url.path)
try:
async with ClientSession() as session:
@ -77,7 +127,6 @@ class AI(FastAPI):
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')):
raise HTTPException(status_code=403, detail="Unauthorized")
"""
TODO: Implement Claude
Currently only routes to local LLM
@ -103,22 +152,20 @@ class AI(FastAPI):
'errorText': 'General Failure'
}
"""
CLAUDE BELOW, COMMENTED
"""
async def ai_hook_handler(self, data: ValidHookSongRequest, request: Request, background_tasks: BackgroundTasks):
"""AI Hook Handler"""
background_tasks.add_task(self.respond_via_webhook, data, request)
return {
'success': True,
}
async def ai_song_handler(self, data: ValidAISongRequest, request: Request):
"""
/ai/song/
AI (Song Info) Request [Public]
"""
ai_prompt = "You are a helpful assistant who will provide tidbits of info on songs the user may listen to."
ai_question = f"I am going to listen to the song \"{data.s}\" by \"{data.a}\"."
local_llm_headers = {
'x-api-key': self.constants.CLAUDE_API_KEY,
'anthropic-version': '2023-06-01',
@ -146,7 +193,8 @@ class AI(FastAPI):
timeout=ClientTimeout(connect=15, sock_read=30)) as request:
await self.glob_state.increment_counter('claude_ai_requests')
response = await request.json()
print(f"Response: {response}")
logging.debug("Response: %s",
response)
if response.get('type') == 'error':
error_type = response.get('error').get('type')
error_message = response.get('error').get('message')
@ -164,55 +212,3 @@ class AI(FastAPI):
'err': True,
'errorText': 'General Failure'
}
# async def ai_song_handler(self, data: ValidAISongRequest, request: Request):
# """
# /ai/song/
# AI (Song Info) Request [Public]
# """
# ai_question = f"I am going to listen to the song \"{data.s}\" by \"{data.a}\"."
# local_llm_headers = {
# 'Authorization': f'Bearer {self.constants.LOCAL_LLM_KEY}'
# }
# ai_req_data = {
# 'max_context_length': 8192,
# 'max_length': 512,
# 'temperature': 0,
# 'n': 1,
# 'top_k': 30,
# 'top_a': 0,
# 'top_p': 0,
# 'typical': 0,
# 'mirostat': 0,
# 'use_default_badwordsids': False,
# 'rep_pen': 1.0,
# 'rep_pen_range': 320,
# 'rep_pen_slope': 0.05,
# 'quiet': 1,
# 'bypass_eos': False,
# # 'trim_stop': True,
# 'sampler_order': [6,0,1,3,4,2,5],
# 'memory': "You are a helpful assistant who will provide ONLY TOTALLY ACCURATE tidbits of info on songs the user may listen to. You do not include information about which album a song was released on, or when it was released, and do not mention that you are not including this information in your response. If the input provided is not a song you are aware of, simply state that. Begin your output at your own response.",
# 'stop': ['### Inst', '### Resp'],
# 'prompt': ai_question
# }
# try:
# async with ClientSession() as session:
# async with await session.post(f'{self.constants.LOCAL_LLM_BASE}/generate',
# json=ai_req_data,
# headers=local_llm_headers,
# timeout=ClientTimeout(connect=15, sock_read=30)) as request:
# await self.glob_state.increment_counter('ai_requests')
# response = await request.json()
# result = {
# 'resp': response.get('results')[0].get('text').strip()
# }
# return result
# except Exception as e: # pylint: disable=broad-exception-caught
# logging.error("Error: %s", e)
# return {
# 'err': True,
# 'errorText': 'General Failure'
# }

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python3.12
from fastapi import FastAPI
from pydantic import BaseModel
@ -31,7 +30,7 @@ class Counters(FastAPI):
self.endpoints = {
"counters/get": self.get_counter_handler,
"counters/increment": self.increment_counter_handler
# "counters/increment": self.increment_counter_handler
#tbd
}
@ -55,14 +54,12 @@ class Counters(FastAPI):
}
async def increment_counter_handler(self, data: ValidCounterIncrementRequest):
"""
/counters/increment/
Increment counter value (requires PUT KEY)
"""
return {
}
# async def increment_counter_handler(self, data: ValidCounterIncrementRequest):
# """
# /counters/increment/
# Increment counter value (requires PUT KEY)
# """
# return {
# }

View File

@ -1,10 +1,12 @@
#!/usr/bin/env python3.12
# pylint: disable=bare-except, broad-exception-caught
import os
import logging
import time
import datetime
import traceback
import aiosqlite as sqlite3
from fastapi import FastAPI, Request, HTTPException
from pydantic import BaseModel
@ -36,31 +38,35 @@ class ValidTopKarmaRequest(BaseModel):
n: int | None = 10
class KarmaDB:
"""Karma DB Util"""
def __init__(self):
self.db_path = os.path.join("/", "var", "lib", "singerdbs", "karma.db")
async def get_karma(self, keyword: str) -> int | dict:
"""Get Karma Value for Keyword"""
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with 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 as e:
except TypeError:
return {
'err': True,
'errorText': f'No records for {keyword}',
}
async def get_top(self, n: int = 10):
"""Get Top n=10 Karma Entries"""
try:
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with db_conn.execute("SELECT keyword, score FROM karma ORDER BY score DESC LIMIT ?", (n,)) as db_cursor:
return await db_cursor.fetchall()
except Exception as e:
print(traceback.format_exc())
except:
traceback.print_exc()
return
async def update_karma(self, granter: str, keyword: str, flag: int):
"""Update Karma for Keyword"""
if not flag in [0, 1]:
return
@ -72,7 +78,7 @@ class KarmaDB:
audit_query = "INSERT INTO karma_audit(impacted_keyword, comment) VALUES(?, ?)"
now = int(time.time())
print(f"Audit message: {audit_message}\nKeyword: {keyword}")
logging.debug("Audit message: %s{audit_message}\nKeyword: %s{keyword}")
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with db_conn.execute(audit_query, (keyword, audit_message,)) as db_cursor:
@ -133,8 +139,8 @@ class Karma(FastAPI):
try:
top10 = await self.db.get_top(n=n)
return top10
except Exception as e:
print(traceback.format_exc())
except:
traceback.print_exc()
return {
'err': True,
'errorText': 'Exception occurred.',
@ -158,7 +164,7 @@ class Karma(FastAPI):
'count': count,
}
except:
print(traceback.format_exc())
traceback.print_exc()
return {
'err': True,
'errorText': "Exception occurred."
@ -183,4 +189,3 @@ class Karma(FastAPI):
return {
'success': await self.db.update_karma(data.granter, data.keyword, data.flag)
}

View File

@ -11,7 +11,7 @@ class ValidArtistSearchRequest(BaseModel):
a: str
class Config:
class Config: # pylint: disable=missing-class-docstring
schema_extra = {
"example": {
"a": "eminem"
@ -27,7 +27,7 @@ class ValidAlbumDetailRequest(BaseModel):
a: str
a2: str
class Config:
class Config: # pylint: disable=missing-class-docstring
schema_extra = {
"example": {
"a": "eminem",
@ -44,7 +44,7 @@ class ValidTrackInfoRequest(BaseModel):
a: str
t: str
class Config:
class Config: # pylint: disable=missing-class-docstring
schema_extra = {
"example": {
"a": "eminem",
@ -185,7 +185,7 @@ class LastFM(FastAPI):
artist = data.a
track = data.t
if not(artist) or not(track):
if not artist or not track:
return {
'err': True,
'errorText': 'Invalid request'

View File

@ -1,10 +1,12 @@
#!/usr/bin/env python3.12
# pylint: disable=bare-except, broad-exception-raised, broad-exception-caught
import importlib
import traceback
import logging
import urllib.parse
import regex
import aiohttp
import traceback
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
@ -92,6 +94,7 @@ class LyricSearch(FastAPI):
}
async def lyric_search_log_handler(self, data: ValidLyricSearchLogRequest):
"""Lyric Search Log Handler"""
include_radio = data.webradio
await self.glob_state.increment_counter('lyrichistory_requests')
last_10k_sings = await self.lyrics_engine.getHistory(limit=10000, webradio=include_radio)
@ -181,7 +184,8 @@ class LyricSearch(FastAPI):
lrc_content = response_json.get('syncedLyrics')
returned_artist = response_json.get('artistName')
returned_song = response_json.get('trackName')
print(f"Synced Lyrics [LRCLib]: {lrc_content}")
logging.debug("Synced Lyrics [LRCLib]: %s",
lrc_content)
lrc_content_out = []
for line in lrc_content.split("\n"):
_timetag = None
@ -192,7 +196,8 @@ class LyricSearch(FastAPI):
if not reg_helper:
continue
reg_helper = reg_helper[0]
print(f"Reg helper: {reg_helper} for line: {line}; len: {len(reg_helper)}")
logging.debug("Reg helper: %s for line: %s; len: %s",
reg_helper, line, len(reg_helper))
_timetag = reg_helper[0]
if not reg_helper[1].strip():
_words = ""
@ -214,7 +219,7 @@ class LyricSearch(FastAPI):
'reqn': await self.glob_state.get_counter('lyric_requests'),
}
except:
print(traceback.format_exc())
traceback.print_exc()
return {
'err': True,
'errorText': 'Search failed!',
@ -225,6 +230,7 @@ class LyricSearch(FastAPI):
'err': True,
'errorText': 'Search failed!',
}
if lrc:
return {
'err': False,
'artist': search_worker['artist'],
@ -236,7 +242,7 @@ class LyricSearch(FastAPI):
'reqn': await self.glob_state.get_counter('lyric_requests'),
}
search_worker = await self.lyrics_engine.lyrics_worker(searching=search_object, recipient='anyone')
search_worker = await self.lyrics_engine.lyrics_worker(searching=search_object)
if not search_worker or not 'l' in search_worker.keys():

View File

@ -2,11 +2,9 @@
import os
import random
import traceback
import aiosqlite as sqlite3
from typing import Optional
from fastapi import FastAPI, Request
import aiosqlite as sqlite3
from fastapi import FastAPI
from pydantic import BaseModel
class RandMsgRequest(BaseModel):

View File

@ -56,7 +56,7 @@ class Transcriptions(FastAPI):
show_id = int(show_id)
if not(str(show_id).isnumeric()) or not(show_id in [0, 1, 2]):
if not(str(show_id).isnumeric()) or show_id not in [0, 1, 2]:
return {
'err': True,
'errorText': 'Show not found.'
@ -99,7 +99,7 @@ class Transcriptions(FastAPI):
"""
show_id = data.s
episode_id = data.e
# pylint: disable=line-too-long
match show_id:
case 0:
db_path = os.path.join("/", "var", "lib", "singerdbs", "sp.db")

View File

@ -1,21 +1,9 @@
#!/usr/bin/env python3.12
# pylint: disable=invalid-name
from fastapi import FastAPI, Request, HTTPException, WebSocket, WebSocketDisconnect, WebSocketException
from fastapi import FastAPI, Request, HTTPException
from pydantic import BaseModel
from aiohttp import ClientSession, ClientTimeout
from aces.connection_manager import ConnectionManager
from aces.flac_reader import AudioStreamer
import os
import asyncio
import pyaudio
import wave
import traceback
import pyflac.decoder as decoder
import numpy as np
import soundfile as sf
import json
import time
class ValidXCRequest(BaseModel):
"""
@ -82,7 +70,7 @@ class XC(FastAPI):
1: '10.10.10.100:5992' # MS & Waleed Combo
}
if not bid in BID_ADDR_MAP.keys():
if not bid in BID_ADDR_MAP:
return {
'err': True,
'errorText': 'Invalid bot id'

View File

@ -43,5 +43,3 @@ class YT(FastAPI):
'video_id': yt_video_id,
'extras': yts_res[0]
}

View File

@ -1,20 +1,24 @@
#!/usr/bin/env python3.11
#!/usr/bin/env python3.12
# pylint: disable=bare-except, broad-exception-caught, invalid-name
from aiohttp import ClientSession, ClientTimeout
import json
import regex
import traceback
import logging
from typing import Union
import regex
from aiohttp import ClientSession, ClientTimeout
from constants import Constants
from typing import Union, Any
class LastFM:
def __init__(self, noInit: Union[None, bool] = False):
"""LastFM Endpoints"""
def __init__(self, noInit: Union[None, bool] = False): # pylint: disable=unused-argument
self.creds = Constants().LFM_CREDS
self.api_base_url = "https://ws.audioscrobbler.com/2.0/?method="
async def search_artist(self, artist=None):
"""Search LastFM for an artist"""
try:
if artist is None:
return {
@ -22,12 +26,13 @@ class LastFM:
}
async with ClientSession() as session:
async with session.get(f"{self.api_base_url}artist.getinfo&artist={artist}&api_key={self.creds.get('key')}&autocorrect=1&format=json", timeout=ClientTimeout(connect=3, sock_read=8)) as request:
async with session.get(f"{self.api_base_url}artist.getinfo&artist={artist}&api_key={self.creds.get('key')}&autocorrect=1&format=json",
timeout=ClientTimeout(connect=3, sock_read=8)) as request:
assert request.status in [200, 204]
data = await request.json()
data = data.get('artist')
print(f"Using data:\n{data}")
logging.debug("Using data:\n%s", data)
# return data.get('results')
retObj = {
'id': data.get('mbid'),
@ -37,21 +42,23 @@ class LastFM:
}
return retObj
except:
print(traceback.format_exc())
traceback.print_exc()
return {
'err': 'Failed'
}
async def get_track_info(self, artist=None, track=None):
"""Get Track Info from LastFM"""
try:
if artist is None or track is None:
print("inv request")
logging.info("inv request")
return {
'err': 'Invalid/No artist or track specified'
}
async with ClientSession() as session:
async with session.get(f"{self.api_base_url}track.getInfo&api_key={self.creds.get('key')}&autocorrect=1&artist={artist}&track={track}&format=json", timeout=ClientTimeout(connect=3, sock_read=8)) as request:
async with session.get(f"{self.api_base_url}track.getInfo&api_key={self.creds.get('key')}&autocorrect=1&artist={artist}&track={track}&format=json",
timeout=ClientTimeout(connect=3, sock_read=8)) as request:
assert request.status in [200, 204]
data = await request.json()
data = data.get('track')
@ -59,18 +66,19 @@ class LastFM:
'artist_mbid': data.get('artist').get('mbid'),
'album': data.get('album').get('title')
}
print(f"Returning:\n{retObj}")
logging.debug("Returning:\n%s", retObj)
return retObj
except:
print(traceback.format_exc())
traceback.print_exc()
return {
'err': 'General Failure'
}
async def get_album_tracklist(self, artist=None, album=None):
"""Get Album Tracklist"""
try:
if artist is None or album is None:
print("inv request")
logging.info("inv request")
return {
'err': 'No artist or album specified'
}
@ -81,16 +89,17 @@ class LastFM:
'tracks': tracks
}
print(f"Returning:\n{retObj}")
logging.debug("Returning:\n%s", retObj)
return retObj
except:
print(traceback.format_exc())
traceback.print_exc()
return {
'err': 'General Failure'
}
async def get_artist_albums(self, artist=None):
"""Get Artists Albums from LastFM"""
try:
if artist is None:
return {
@ -98,7 +107,8 @@ class LastFM:
}
async with ClientSession() as session:
async with 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:
async with 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:
assert request.status in [200, 204]
# return request.text
data = await request.json()
@ -107,17 +117,18 @@ class LastFM:
retObj = [
{
'title': item.get('name')
} for item in data if not(item.get('name').lower() == "(null)") and not(int(item.get('playcount')) < 50)
} for item in data if not(item.get('name').lower() == "(null)") and int(item.get('playcount')) >= 50
]
# # print(f"Keys: {data[0].keys()}")
return retObj
except:
print(traceback.format_exc())
traceback.print_exc()
return {
'err': 'Failed'
}
async def get_artist_id(self, artist=None):
"""Get Artist ID from LastFM"""
try:
if artist is None:
return {
@ -125,27 +136,29 @@ class LastFM:
}
artist_search = await self.search_artist(artist=artist)
if artist_search is None or len(artist_search) < 1:
print("[get_artist_id] Throwing no result error")
logging.debug("[get_artist_id] Throwing no result error")
return {
'err': 'No results.'
}
artist_id = artist_search[0].get('id')
return artist_id
except:
print(traceback.format_exc())
traceback.print_exc()
return {
'err': 'Failed'
}
async def get_artist_info_by_id(self, artist_id=None):
"""Get Artist info by ID from LastFM"""
try:
if artist_id is None or not(str(artist_id).isnumeric()):
if artist_id is None or not str(artist_id).isnumeric():
return {
'err': 'Invalid/no artist_id specified.'
}
async with ClientSession() as session:
async with session.get(f"{self.api_base_url}artists/{artist_id}?key={self.creds.get('key')}&secret={self.creds.get('secret')}", timeout=ClientTimeout(connect=3, sock_read=8)) as request:
async with session.get(f"{self.api_base_url}artists/{artist_id}?key={self.creds.get('key')}&secret={self.creds.get('secret')}",
timeout=ClientTimeout(connect=3, sock_read=8)) as request:
assert request.status in [200, 204]
data = await request.json()
@ -157,35 +170,37 @@ class LastFM:
}
return retObj
except:
print(traceback.format_exc())
traceback.print_exc()
return {
'err': 'Failed'
}
async def get_artist_info(self, artist=None):
"""Get Artist Info from LastFM"""
try:
if artist is None:
return {
'err': 'No artist specified.'
}
# artist_id = await self.get_artist_id(artist=artist)
# if artist_id is None:
# return {
# 'err': Failed
# }
artist_id = await self.get_artist_id(artist=artist)
if artist_id is None:
return {
'err': 'Failed',
}
artist_info = await self.get_artist_info_by_id(artist_id=artist_id)
if artist_info is None:
return {
'err': Failed
'err': 'Failed',
}
return artist_info
except:
print(traceback.format_exc())
traceback.print_exc()
return {
'err': 'Failed'
}
async def get_release(self, artist=None, album=None):
"""Get Release info from LastFM"""
try:
if artist is None or album is None:
return {
@ -193,7 +208,8 @@ class LastFM:
}
async with ClientSession() as session:
async with session.get(f"{self.api_base_url}album.getinfo&artist={artist}&album={album}&api_key={self.creds.get('key')}&autocorrect=1&format=json", timeout=ClientTimeout(connect=3, sock_read=8)) as request:
async with session.get(f"{self.api_base_url}album.getinfo&artist={artist}&album={album}&api_key={self.creds.get('key')}&autocorrect=1&format=json",
timeout=ClientTimeout(connect=3, sock_read=8)) as request:
assert request.status in [200, 204]
data = await request.json()
data = data.get('album')
@ -206,9 +222,10 @@ class LastFM:
}
try:
track_key = data.get('tracks').get('track')
except: track_key = []
if type(track_key) == list:
print(f"Track key: {track_key}")
except:
track_key = []
if isinstance(track_key, list):
logging.debug("Track key: %s", track_key)
retObj['tracks'] = [
{
'duration': item.get('duration', 'N/A'),
@ -223,7 +240,7 @@ class LastFM:
]
return retObj
except:
print(traceback.format_exc())
traceback.print_exc()
return {
'err': 'Failed'
}

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3.12
# pylint: disable=bare-except, broad-exception-raised
"""Global State Storage/Counters"""
@ -11,7 +12,9 @@ from fastapi_utils.tasks import repeat_every
class State(FastAPI):
def __init__(self, app: FastAPI, util, constants):
"""Global State for API"""
def __init__(self, app: FastAPI, util, constants): # pylint: disable=unused-argument
super().__init__()
self.counter_db_path = os.path.join("/", "var", "lib", "singerdbs", "stats.db")
self.counters = {
str(counter): 0 for counter in constants.AVAILABLE_COUNTERS
@ -53,7 +56,7 @@ class State(FastAPI):
@app.on_event("startup")
@repeat_every(seconds=10)
async def update_db():
if self.counters_initialized == False:
if not self.counters_initialized:
logging.debug("[State] TICK: Counters not yet initialized")
return
@ -92,21 +95,20 @@ class State(FastAPI):
async def increment_counter(self, counter: str):
if not(counter in self.counters.keys()):
raise BaseException("[State] Counter %s does not exist", counter)
"""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):
if not(counter in self.counters.keys()):
raise BaseException("[State] Counter %s does not exist", counter)
"""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

11
util.py
View File

@ -2,21 +2,25 @@
import logging
from fastapi import FastAPI, Response, HTTPException, Security
from fastapi import FastAPI, Response, HTTPException
class Utilities:
"""API Utilities"""
def __init__(self, app: FastAPI, constants):
self.constants = constants
self.blocked_response_status = 422
self.blocked_response_content = None
self.app = app
def get_blocked_response(self, path: str | None = None):
def get_blocked_response(self, path: str | None = None): # pylint: disable=unused-argument
"""Get Blocked HTTP Response"""
logging.error("Rejected request: Blocked")
return Response(content=self.blocked_response_content,
status_code=self.blocked_response_status)
def get_no_endpoint_found(self, path: str | None = None):
def get_no_endpoint_found(self, path: str | None = None): # pylint: disable=unused-argument
"""Get 404 Response"""
logging.error("Rejected request: No such endpoint")
raise HTTPException(detail="Unknown endpoint", status_code=404)
@ -49,4 +53,3 @@ class Utilities:
# print("Auth succeeded")
return True