stoof
This commit is contained in:
parent
57f5564fe6
commit
6b62757dad
23
aces/connection_manager.py
Normal file
23
aces/connection_manager.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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}")
|
||||||
|
|
75
aces/flac_reader.py
Normal file
75
aces/flac_reader.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
#!/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)
|
3
base.py
3
base.py
@ -33,6 +33,7 @@ glob_state = importlib.import_module("state").State(app, util, constants)
|
|||||||
|
|
||||||
origins = [
|
origins = [
|
||||||
"https://codey.lol",
|
"https://codey.lol",
|
||||||
|
"https://api.codey.lol"
|
||||||
]
|
]
|
||||||
|
|
||||||
app.add_middleware(CORSMiddleware,
|
app.add_middleware(CORSMiddleware,
|
||||||
@ -84,8 +85,6 @@ lastfm_endpoints = importlib.import_module("endpoints.lastfm").LastFM(app, util,
|
|||||||
yt_endpoints = importlib.import_module("endpoints.yt").YT(app, util, constants, glob_state)
|
yt_endpoints = importlib.import_module("endpoints.yt").YT(app, util, constants, glob_state)
|
||||||
# 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, glob_state)
|
||||||
# Below: CAH endpoint(s)
|
|
||||||
# cah_endpoints = importlib.import_module("endpoints.cah").CAH(app, util, constants, glob_state)
|
|
||||||
|
|
||||||
# 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, glob_state)
|
||||||
|
@ -40,6 +40,7 @@ class AI(FastAPI):
|
|||||||
"""
|
"""
|
||||||
/ai/base/
|
/ai/base/
|
||||||
AI BASE Request
|
AI BASE Request
|
||||||
|
(Requires key)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')):
|
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')):
|
||||||
@ -70,6 +71,7 @@ class AI(FastAPI):
|
|||||||
"""
|
"""
|
||||||
/ai/openai/
|
/ai/openai/
|
||||||
AI Request
|
AI Request
|
||||||
|
(Requires key)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')):
|
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')):
|
||||||
@ -145,6 +147,13 @@ class AI(FastAPI):
|
|||||||
await self.glob_state.increment_counter('claude_ai_requests')
|
await self.glob_state.increment_counter('claude_ai_requests')
|
||||||
response = await request.json()
|
response = await request.json()
|
||||||
print(f"Response: {response}")
|
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 = {
|
result = {
|
||||||
'resp': response.get('content')[0].get('text').strip()
|
'resp': response.get('content')[0].get('text').strip()
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
import importlib
|
import importlib
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import regex
|
import regex
|
||||||
|
import aiohttp
|
||||||
|
import traceback
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@ -14,6 +16,7 @@ class ValidLyricRequest(BaseModel):
|
|||||||
- **s**: song
|
- **s**: song
|
||||||
- **t**: track (artist and song combined) [used only if a & s are not used]
|
- **t**: track (artist and song combined) [used only if a & s are not used]
|
||||||
- **extra**: include extra details in response [optional, default: false]
|
- **extra**: include extra details in response [optional, default: false]
|
||||||
|
- **lrc**: Request LRCs?
|
||||||
- **sub**: text to search within lyrics, if found lyrics will begin at found verse [optional]
|
- **sub**: text to search within lyrics, if found lyrics will begin at found verse [optional]
|
||||||
- **src**: the script/utility which initiated the request
|
- **src**: the script/utility which initiated the request
|
||||||
"""
|
"""
|
||||||
@ -23,6 +26,7 @@ class ValidLyricRequest(BaseModel):
|
|||||||
t: str | None = None
|
t: str | None = None
|
||||||
sub: str | None = None
|
sub: str | None = None
|
||||||
extra: bool | None = False
|
extra: bool | None = False
|
||||||
|
lrc: bool | None = False
|
||||||
src: str
|
src: str
|
||||||
|
|
||||||
class Config: # pylint: disable=missing-class-docstring too-few-public-methods
|
class Config: # pylint: disable=missing-class-docstring too-few-public-methods
|
||||||
@ -31,38 +35,12 @@ class ValidLyricRequest(BaseModel):
|
|||||||
"a": "eminem",
|
"a": "eminem",
|
||||||
"s": "rap god",
|
"s": "rap god",
|
||||||
"src": "WEB",
|
"src": "WEB",
|
||||||
"extra": True
|
"extra": True,
|
||||||
|
"lrc": False,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ValidLyricRequest(BaseModel):
|
|
||||||
"""
|
|
||||||
- **a**: artist
|
|
||||||
- **s**: song
|
|
||||||
- **t**: track (artist and song combined) [used only if a & s are not used]
|
|
||||||
- **extra**: include extra details in response [optional, default: false]
|
|
||||||
- **sub**: text to search within lyrics, if found lyrics will begin at found verse [optional]
|
|
||||||
- **src**: the script/utility which initiated the request
|
|
||||||
"""
|
|
||||||
|
|
||||||
a: str | None = None
|
|
||||||
s: str | None = None
|
|
||||||
t: str | None = None
|
|
||||||
sub: str | None = None
|
|
||||||
extra: bool | None = False
|
|
||||||
src: str
|
|
||||||
|
|
||||||
class Config: # pylint: disable=missing-class-docstring too-few-public-methods
|
|
||||||
schema_extra = {
|
|
||||||
"example": {
|
|
||||||
"a": "eminem",
|
|
||||||
"s": "rap god",
|
|
||||||
"src": "WEB",
|
|
||||||
"extra": True
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ValidLyricSearchLogRequest(BaseModel):
|
class ValidLyricSearchLogRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
- **webradio**: whether or not to include requests generated automatically by the radio page on codey.lol, defaults to False
|
- **webradio**: whether or not to include requests generated automatically by the radio page on codey.lol, defaults to False
|
||||||
@ -99,6 +77,8 @@ class LyricSearch(FastAPI):
|
|||||||
"IRC-SHARED"
|
"IRC-SHARED"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
self.lrc_regex = regex.compile(r'\[([0-9]{2}:[0-9]{2})\.[0-9]{1,3}\](\s(.*)){0,}')
|
||||||
|
|
||||||
for endpoint, handler in self.endpoints.items():
|
for endpoint, handler in self.endpoints.items():
|
||||||
app.add_api_route(f"/{endpoint}/", handler, methods=["POST"])
|
app.add_api_route(f"/{endpoint}/", handler, methods=["POST"])
|
||||||
|
|
||||||
@ -129,10 +109,12 @@ class LyricSearch(FastAPI):
|
|||||||
- **s**: song
|
- **s**: song
|
||||||
- **t**: track (artist and song combined) [used only if a & s are not used]
|
- **t**: track (artist and song combined) [used only if a & s are not used]
|
||||||
- **extra**: include extra details in response [optional, default: false]
|
- **extra**: include extra details in response [optional, default: false]
|
||||||
|
- **lrc**: Request LRCs?
|
||||||
- **sub**: text to search within lyrics, if found lyrics will begin at found verse [optional, default: none]
|
- **sub**: text to search within lyrics, if found lyrics will begin at found verse [optional, default: none]
|
||||||
- **src**: the script/utility which initiated the request
|
- **src**: the script/utility which initiated the request
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
lrc = data.lrc
|
||||||
src = data.src.upper()
|
src = data.src.upper()
|
||||||
if not src in self.acceptable_request_sources:
|
if not src in self.acceptable_request_sources:
|
||||||
raise HTTPException(detail="Invalid request source", status_code=403)
|
raise HTTPException(detail="Invalid request source", status_code=403)
|
||||||
@ -175,9 +157,89 @@ class LyricSearch(FastAPI):
|
|||||||
else:
|
else:
|
||||||
search_object = self.lyrics_engine.create_query_object(str(search_text))
|
search_object = self.lyrics_engine.create_query_object(str(search_text))
|
||||||
|
|
||||||
|
if lrc:
|
||||||
|
search_worker = await self.lyrics_engine.grabFromSpotify(searching=search_object,
|
||||||
|
lrc=True)
|
||||||
|
|
||||||
|
spotify_lyrics_unsynced = True
|
||||||
|
if search_worker and search_worker.get('l'):
|
||||||
|
for line in search_worker.get('l'):
|
||||||
|
if line.get('timeTag') and line.get('timeTag') != "00:00.00":
|
||||||
|
spotify_lyrics_unsynced = False
|
||||||
|
if not search_worker or spotify_lyrics_unsynced:
|
||||||
|
# Try LRCLib before failing out
|
||||||
|
try:
|
||||||
|
lrclib_api_url = "https://lrclib.net/api/get"
|
||||||
|
sane_artist = urllib.parse.quote_plus(search_artist)
|
||||||
|
sane_track = urllib.parse.quote_plus(search_song)
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(f"{lrclib_api_url}?artist_name={sane_artist}&track_name={sane_track}") as request:
|
||||||
|
request.raise_for_status()
|
||||||
|
response_json = await request.json()
|
||||||
|
if not "syncedLyrics" in response_json:
|
||||||
|
raise BaseException("LRCLib Fallback Failed")
|
||||||
|
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}")
|
||||||
|
lrc_content_out = []
|
||||||
|
for line in lrc_content.split("\n"):
|
||||||
|
_timetag = None
|
||||||
|
_words = None
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
reg_helper = regex.findall(self.lrc_regex, line.strip())
|
||||||
|
if not reg_helper:
|
||||||
|
continue
|
||||||
|
reg_helper = reg_helper[0]
|
||||||
|
print(f"Reg helper: {reg_helper} for line: {line}; len: {len(reg_helper)}")
|
||||||
|
_timetag = reg_helper[0]
|
||||||
|
if not reg_helper[1].strip():
|
||||||
|
_words = "♪"
|
||||||
|
else:
|
||||||
|
_words = reg_helper[1]
|
||||||
|
lrc_content_out.append({
|
||||||
|
"timeTag": _timetag,
|
||||||
|
"words": _words,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'err': False,
|
||||||
|
'artist': returned_artist,
|
||||||
|
'song': returned_song,
|
||||||
|
'combo_lev': "N/A",
|
||||||
|
'lrc': lrc_content_out,
|
||||||
|
'from_cache': False,
|
||||||
|
'src': 'Alt LRC SRC',
|
||||||
|
'reqn': await self.glob_state.get_counter('lyric_requests'),
|
||||||
|
}
|
||||||
|
except:
|
||||||
|
print(traceback.format_exc())
|
||||||
|
return {
|
||||||
|
'err': True,
|
||||||
|
'errorText': 'Search failed!',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
'err': True,
|
||||||
|
'errorText': 'Search failed!',
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'err': False,
|
||||||
|
'artist': search_worker['artist'],
|
||||||
|
'song': search_worker['song'],
|
||||||
|
'combo_lev': search_worker['combo_lev'],
|
||||||
|
'lrc': search_worker['l'],
|
||||||
|
'from_cache': False,
|
||||||
|
'src': search_worker['method'],
|
||||||
|
'reqn': await self.glob_state.get_counter('lyric_requests'),
|
||||||
|
}
|
||||||
|
|
||||||
search_worker = await self.lyrics_engine.lyrics_worker(searching=search_object,
|
search_worker = await self.lyrics_engine.lyrics_worker(searching=search_object,
|
||||||
recipient='anyone')
|
recipient='anyone')
|
||||||
|
|
||||||
|
|
||||||
if not search_worker or not 'l' in search_worker.keys():
|
if not search_worker or not 'l' in search_worker.keys():
|
||||||
await self.glob_state.increment_counter('failedlyric_requests')
|
await self.glob_state.increment_counter('failedlyric_requests')
|
||||||
return {
|
return {
|
||||||
|
@ -1,8 +1,21 @@
|
|||||||
#!/usr/bin/env python3.12
|
#!/usr/bin/env python3.12
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, HTTPException
|
from fastapi import FastAPI, Request, HTTPException, WebSocket, WebSocketDisconnect, WebSocketException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from aiohttp import ClientSession, ClientTimeout
|
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):
|
class ValidXCRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
@ -17,6 +30,8 @@ class ValidXCRequest(BaseModel):
|
|||||||
cmd: str
|
cmd: str
|
||||||
data: dict | None = None
|
data: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class XC(FastAPI):
|
class XC(FastAPI):
|
||||||
"""XC (CrossComm) Endpoints"""
|
"""XC (CrossComm) Endpoints"""
|
||||||
def __init__(self, app: FastAPI, util, constants, glob_state): # pylint: disable=super-init-not-called
|
def __init__(self, app: FastAPI, util, constants, glob_state): # pylint: disable=super-init-not-called
|
||||||
@ -25,14 +40,26 @@ class XC(FastAPI):
|
|||||||
self.constants = constants
|
self.constants = constants
|
||||||
self.glob_state = glob_state
|
self.glob_state = glob_state
|
||||||
|
|
||||||
|
self.ws_endpoints = {
|
||||||
|
# "aces_ws_put": self.put_ws_handler,
|
||||||
|
}
|
||||||
|
|
||||||
self.endpoints = {
|
self.endpoints = {
|
||||||
"xc": self.xc_handler,
|
"xc": self.xc_handler,
|
||||||
#tbd
|
#tbd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for endpoint, handler in self.ws_endpoints.items():
|
||||||
|
app.add_api_websocket_route(f"/{endpoint}/", handler)
|
||||||
|
|
||||||
for endpoint, handler in self.endpoints.items():
|
for endpoint, handler in self.endpoints.items():
|
||||||
app.add_api_route(f"/{endpoint}/", handler, methods=["POST"])
|
app.add_api_route(f"/{endpoint}/", handler, methods=["POST"])
|
||||||
|
|
||||||
|
# async def put_ws_handler(self, ws: WebSocket):
|
||||||
|
# await ws.accept()
|
||||||
|
# await self.audio_streamer.handle_client(ws)
|
||||||
|
|
||||||
async def xc_handler(self, data: ValidXCRequest, request: Request):
|
async def xc_handler(self, data: ValidXCRequest, request: Request):
|
||||||
"""
|
"""
|
||||||
/xc/
|
/xc/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user