diff --git a/aces/connection_manager.py b/aces/connection_manager.py deleted file mode 100644 index fa55569..0000000 --- a/aces/connection_manager.py +++ /dev/null @@ -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}") - diff --git a/aces/flac_reader.py b/aces/flac_reader.py deleted file mode 100644 index d495277..0000000 --- a/aces/flac_reader.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/base.py b/base.py index cf128b2..594a478 100644 --- a/base.py +++ b/base.py @@ -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 diff --git a/cah_ext/__init__.py b/cah_ext/__init__.py new file mode 100644 index 0000000..b4ecc12 --- /dev/null +++ b/cah_ext/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3.12 + +from . import constructors \ No newline at end of file diff --git a/cah_ext/constructors.py b/cah_ext/constructors.py new file mode 100644 index 0000000..f5a6ba5 --- /dev/null +++ b/cah_ext/constructors.py @@ -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 + + + \ No newline at end of file diff --git a/cah_ext/websocket_conn.py b/cah_ext/websocket_conn.py new file mode 100644 index 0000000..33c7d8a --- /dev/null +++ b/cah_ext/websocket_conn.py @@ -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 \ No newline at end of file diff --git a/endpoints/ai.py b/endpoints/ai.py index f2ca5f1..1c05cba 100644 --- a/endpoints/ai.py +++ b/endpoints/ai.py @@ -1,25 +1,39 @@ #!/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 - **s**: track title """ + + 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""" + """AI Endpoints""" def __init__(self, app: FastAPI, my_util, constants, glob_state): # pylint: disable=super-init-not-called self.app = app self.util = my_util @@ -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): """ @@ -42,83 +92,80 @@ class AI(FastAPI): AI BASE Request (Requires key) """ - + 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: - async with await session.post(f'{self.constants.LOCAL_LLM_BASE}/{forward_path}', - json=await request.json(), - headers=local_llm_headers, - timeout=ClientTimeout(connect=15, sock_read=30)) as out_request: - await self.glob_state.increment_counter('ai_requests') - response = await out_request.json() - return response + async with ClientSession() as session: + async with await session.post(f'{self.constants.LOCAL_LLM_BASE}/{forward_path}', + json=await request.json(), + headers=local_llm_headers, + timeout=ClientTimeout(connect=15, sock_read=30)) as out_request: + await self.glob_state.increment_counter('ai_requests') + response = await out_request.json() + return response except Exception as e: # pylint: disable=broad-exception-caught - logging.error("Error: %s", e) - return { - 'err': True, - 'errorText': 'General Failure' - } - + logging.error("Error: %s", e) + return { + 'err': True, + 'errorText': 'General Failure' + } + async def ai_openai_handler(self, request: Request): """ /ai/openai/ AI Request (Requires key) """ - + 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 """ - + 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: - async with await session.post(f'{self.constants.LOCAL_LLM_HOST}/{forward_path}', - json=await request.json(), - headers=local_llm_headers, - timeout=ClientTimeout(connect=15, sock_read=30)) as out_request: - await self.glob_state.increment_counter('ai_requests') - response = await out_request.json() - return response + async with ClientSession() as session: + async with await session.post(f'{self.constants.LOCAL_LLM_HOST}/{forward_path}', + json=await request.json(), + headers=local_llm_headers, + timeout=ClientTimeout(connect=15, sock_read=30)) as out_request: + await self.glob_state.increment_counter('ai_requests') + response = await out_request.json() + return response except Exception as e: # pylint: disable=broad-exception-caught - logging.error("Error: %s", e) - return { - 'err': True, - 'errorText': 'General Failure' - } + logging.error("Error: %s", e) + return { + 'err': True, + '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', @@ -126,93 +173,42 @@ class AI(FastAPI): } request_data = { - 'model': 'claude-3-haiku-20240307', - 'max_tokens': 512, - 'temperature': 0.6, - 'system': ai_prompt, - 'messages': [ - { - "role": "user", - "content": ai_question.strip(), - } - ] + 'model': 'claude-3-haiku-20240307', + 'max_tokens': 512, + 'temperature': 0.6, + 'system': ai_prompt, + 'messages': [ + { + "role": "user", + "content": ai_question.strip(), + } + ] } try: - async with ClientSession() as session: - async with await session.post('https://api.anthropic.com/v1/messages', - json=request_data, - headers=local_llm_headers, - 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}") - if response.get('type') == 'error': - error_type = response.get('error').get('type') - error_message = response.get('error').get('message') - result = { - 'resp': f"{error_type} error ({error_message})" - } - else: - result = { - 'resp': response.get('content')[0].get('text').strip() - } - return result + async with ClientSession() as session: + async with await session.post('https://api.anthropic.com/v1/messages', + json=request_data, + headers=local_llm_headers, + timeout=ClientTimeout(connect=15, sock_read=30)) as request: + await self.glob_state.increment_counter('claude_ai_requests') + response = await request.json() + logging.debug("Response: %s", + response) + if response.get('type') == 'error': + error_type = response.get('error').get('type') + error_message = response.get('error').get('message') + result = { + 'resp': f"{error_type} error ({error_message})" + } + else: + result = { + 'resp': response.get('content')[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' - } - - # 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' - # } \ No newline at end of file + logging.error("Error: %s", e) + return { + 'err': True, + 'errorText': 'General Failure' + } \ No newline at end of file diff --git a/endpoints/counters.py b/endpoints/counters.py index 6988f9d..2284cb8 100644 --- a/endpoints/counters.py +++ b/endpoints/counters.py @@ -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 { + # } diff --git a/endpoints/karma.py b/endpoints/karma.py index a71c653..754b058 100644 --- a/endpoints/karma.py +++ b/endpoints/karma.py @@ -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." @@ -182,5 +188,4 @@ class Karma(FastAPI): return { 'success': await self.db.update_karma(data.granter, data.keyword, data.flag) - } - + } \ No newline at end of file diff --git a/endpoints/lastfm.py b/endpoints/lastfm.py index 5baf66b..5954aff 100644 --- a/endpoints/lastfm.py +++ b/endpoints/lastfm.py @@ -10,13 +10,13 @@ class ValidArtistSearchRequest(BaseModel): """ a: str - - class Config: - schema_extra = { - "example": { - "a": "eminem" - } - } + + class Config: # pylint: disable=missing-class-docstring + schema_extra = { + "example": { + "a": "eminem" + } + } class ValidAlbumDetailRequest(BaseModel): """ @@ -27,12 +27,12 @@ class ValidAlbumDetailRequest(BaseModel): a: str a2: str - class Config: - schema_extra = { - "example": { - "a": "eminem", - "a2": "houdini" - } + class Config: # pylint: disable=missing-class-docstring + schema_extra = { + "example": { + "a": "eminem", + "a2": "houdini" + } } class ValidTrackInfoRequest(BaseModel): @@ -44,12 +44,12 @@ class ValidTrackInfoRequest(BaseModel): a: str t: str - class Config: - schema_extra = { - "example": { - "a": "eminem", - "t": "rap god" - } + class Config: # pylint: disable=missing-class-docstring + schema_extra = { + "example": { + "a": "eminem", + "t": "rap god" + } } class LastFM(FastAPI): @@ -185,11 +185,11 @@ class LastFM(FastAPI): artist = data.a track = data.t - if not(artist) or not(track): - return { + if not artist or not track: + return { 'err': True, 'errorText': 'Invalid request' - } + } track_info_result = await self.lastfm.get_track_info(artist=artist, track=track) assert not "err" in track_info_result.keys() diff --git a/endpoints/lyric_search.py b/endpoints/lyric_search.py index 4d2ec22..864aed7 100644 --- a/endpoints/lyric_search.py +++ b/endpoints/lyric_search.py @@ -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(): diff --git a/endpoints/rand_msg.py b/endpoints/rand_msg.py index 690c8fc..8e23ba7 100644 --- a/endpoints/rand_msg.py +++ b/endpoints/rand_msg.py @@ -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): diff --git a/endpoints/transcriptions.py b/endpoints/transcriptions.py index d8e5c8a..866c71a 100644 --- a/endpoints/transcriptions.py +++ b/endpoints/transcriptions.py @@ -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") diff --git a/endpoints/xc.py b/endpoints/xc.py index 5cee27b..def0462 100644 --- a/endpoints/xc.py +++ b/endpoints/xc.py @@ -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' diff --git a/endpoints/yt.py b/endpoints/yt.py index cf1c30b..e88280f 100644 --- a/endpoints/yt.py +++ b/endpoints/yt.py @@ -42,6 +42,4 @@ class YT(FastAPI): return { 'video_id': yt_video_id, 'extras': yts_res[0] - } - - + } \ No newline at end of file diff --git a/lastfm_wrapper.py b/lastfm_wrapper.py index 7adc4f3..b05dc71 100644 --- a/lastfm_wrapper.py +++ b/lastfm_wrapper.py @@ -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' } diff --git a/state.py b/state.py index cf05355..b3c9a78 100644 --- a/state.py +++ b/state.py @@ -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): - return self.counters - - - - + """Get All Counters""" + return self.counters \ No newline at end of file diff --git a/util.py b/util.py index affa37b..70bae33 100644 --- a/util.py +++ b/util.py @@ -2,21 +2,25 @@ import logging -from fastapi import FastAPI, Response, HTTPException, Security +from fastapi import FastAPI, Response, HTTPException class Utilities: - def __init__(self, app: FastAPI, constants): + """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) @@ -48,5 +52,4 @@ class Utilities: return False # print("Auth succeeded") - return True - + return True \ No newline at end of file