changes include: allow GET endpoints, redirect to codey.lol instead of returning status 422, clean junk from ai endpoint, add misc endpoint, add methods to lyric_search_new.sources.cache, other misc/cleanup

This commit is contained in:
codey 2025-01-21 19:16:23 -05:00
parent e95ef3b088
commit 38dbddd297
7 changed files with 148 additions and 23 deletions

22
base.py
View File

@ -4,7 +4,7 @@ import importlib
import logging
import asyncio
from typing import Any
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from lyric_search_new.sources import redis_cache
@ -34,7 +34,7 @@ origins = [
app.add_middleware(CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["POST"],
allow_methods=["POST", "GET", "HEAD"],
allow_headers=["*"])
@ -49,9 +49,18 @@ Blacklisted routes
def disallow_get():
return util.get_blocked_response()
@app.get("/{any:path}")
def disallow_get_any(var: Any = None): # pylint: disable=unused-argument
return util.get_blocked_response()
@app.get("/{path}")
def disallow_get_any(request: Request, var: Any = None): # pylint: disable=unused-argument
path = request.path_params['path']
if not (
isinstance(path, str)
and
path.split("/", maxsplit=1) == "widget"
):
return util.get_blocked_response()
else:
logging.info("OK, %s",
path)
@app.post("/")
def disallow_base_post():
@ -79,9 +88,10 @@ lastfm_endpoints = importlib.import_module("endpoints.lastfm").LastFM(app, util,
yt_endpoints = importlib.import_module("endpoints.yt").YT(app, util, constants, glob_state)
# Below: XC endpoint(s)
xc_endpoints = importlib.import_module("endpoints.xc").XC(app, util, constants, glob_state)
# Below: Karma endpoint(s)
karma_endpoints = importlib.import_module("endpoints.karma").Karma(app, util, constants, glob_state)
# Below: Misc endpoints
misc_endpoints = importlib.import_module("endpoints.misc").Misc(app, util, constants, glob_state)
"""
End Actionable Routes

View File

@ -4,6 +4,7 @@
import logging
import traceback
import regex
import json
from aiohttp import ClientSession, ClientTimeout
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
@ -49,7 +50,8 @@ class AI(FastAPI):
}
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"] if not endpoint == "testy" else ["POST", "GET"])
async def respond_via_webhook(self, data: ValidHookSongRequest, originalRequest: Request):
"""Respond via Webhook"""

77
endpoints/misc.py Normal file
View File

@ -0,0 +1,77 @@
#!/usr/bin/env python3.12
# pylint: disable=bare-except, broad-exception-caught, invalid-name
import logging
import traceback
import time
from fastapi import FastAPI
import redis.asyncio as redis
from redis.commands.search.query import Query
from redis.commands.search.indexDefinition import IndexDefinition, IndexType
from redis.commands.search.field import TextField, TagField
from redis.commands.json.path import Path
from lyric_search_new.sources import private, cache as LyricsCache
class Misc(FastAPI):
"""Misc Endpoints"""
def __init__(self, app: FastAPI, my_util, constants, glob_state): # pylint: disable=super-init-not-called
self.app = app
self.util = my_util
self.constants = constants
self.glob_state = glob_state
self.lyr_cache = LyricsCache.Cache()
self.redis_client = redis.Redis(password=private.REDIS_PW)
self.endpoints = {
"widget/redis": self.homepage_redis_widget,
"widget/sqlite": self.homepage_sqlite_widget,
}
for endpoint, handler in self.endpoints.items():
app.add_api_route(f"/{endpoint}/", handler, methods=["GET"])
async def homepage_redis_widget(self) -> dict:
"""
/widget/redis/
Homepage Widget Handler
Args:
None
Returns:
dict
"""
# Measure response time w/ test lyric search
time_start: float = time.time() # Start time for response_time
test_lyrics_result = await self.redis_client.ft().search("@artist: test @song: test")
time_end: float = time.time()
# End response time test
total_keys = await self.redis_client.dbsize()
response_time: float = time_end - time_start
(_, ci_keys) = await self.redis_client.scan(cursor=0, match="ci_session*", count=10000000)
num_ci_keys = len(ci_keys)
index_info = await self.redis_client.ft().info()
indexed_lyrics = index_info.get('num_docs')
return {
'responseTime': round(response_time, 7),
'storedKeys': total_keys,
'indexedLyrics': indexed_lyrics,
'sessions': num_ci_keys,
}
async def homepage_sqlite_widget(self) -> dict:
"""
/widget/sqlite/
Homepage Widget Handler
Args:
None
Returns:
dict
"""
row_count = await self.lyr_cache.sqlite_rowcount()
distinct_artists = await self.lyr_cache.sqlite_distinct("artist")
lyrics_length = await self.lyr_cache.sqlite_lyrics_length()
return {
'storedRows': row_count,
'distinctArtists': distinct_artists,
'lyricsLength': lyrics_length,
}

View File

@ -110,16 +110,13 @@ class LastFM:
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()
data = data.get('topalbums').get('album')
# print(f"Data:\n{data}")
retObj = [
{
'title': item.get('name')
} 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:
traceback.print_exc()

View File

@ -50,7 +50,6 @@ class Cache:
for res in redis_results:
(key, row) = res
if key == matched_id:
logging.info("Matched row: %s", row)
return LyricsResult(
artist=row['artist'],
song=row['song'],
@ -114,6 +113,53 @@ class Cache:
await self.redis_cache.redis_store(sqlite_insert_id, lyr_result)
async def sqlite_rowcount(self, where: Optional[str] = None, params: Optional[tuple] = None) -> int:
"""
Get rowcount for cached_lyrics DB
Args:
where (Optional[str]): WHERE ext for query if needed
params (Optional[tuple]): Parameters to query, if where is specified
Returns:
int: Number of rows found
"""
async with sqlite3.connect(self.cache_db, timeout=2) as db_conn:
db_conn.row_factory = sqlite3.Row
query = f"SELECT count(id) AS rowcount FROM lyrics {where}".strip()
async with await db_conn.execute(query, params) as db_cursor:
result = await db_cursor.fetchone()
return result['rowcount']
async def sqlite_distinct(self, column: str) -> int:
"""
Get count of distinct values for a column
Args:
column (str): The column to check
Returns:
int: Number of distinct values found
"""
async with sqlite3.connect(self.cache_db, timeout=2) as db_conn:
db_conn.row_factory = sqlite3.Row
query = f"SELECT COUNT(DISTINCT {column}) as distinct_items FROM lyrics"
async with await db_conn.execute(query) as db_cursor:
result = await db_cursor.fetchone()
return result['distinct_items']
async def sqlite_lyrics_length(self) -> int:
"""
Get total length of text stored for lyrics
Args:
None
Returns:
int: Total length of stored lyrics
"""
async with sqlite3.connect(self.cache_db, timeout=2) as db_conn:
db_conn.row_factory = sqlite3.Row
query = "SELECT SUM(LENGTH(lyrics)) as lyrics_len FROM lyrics"
async with await db_conn.execute(query) as db_cursor:
result = await db_cursor.fetchone()
return result['lyrics_len']
async def sqlite_store(self, lyr_result: LyricsResult) -> int:
"""
Store lyrics to SQLite Cache
@ -188,7 +234,7 @@ class Cache:
"""Check Redis First"""
logging.info("Checking redis cache for %s...",
logging.debug("Checking redis cache for %s...",
f"{artist} - {song}")
redis_result = await self.redis_cache.search(artist=artist,
song=song)

View File

@ -105,7 +105,7 @@ class RedisCache:
if not is_random_search:
logging.debug("Redis: Searching normally first")
(artist, song) = self.sanitize_input(artist, song)
logging.info("Seeking: %s - %s", artist, song)
logging.debug("Seeking: %s - %s", artist, song)
search_res = await self.redis_client.ft().search(Query(
f"@artist:{artist} @song:{song}"
))

13
util.py
View File

@ -3,21 +3,20 @@
import logging
from fastapi import FastAPI, Response, HTTPException
from fastapi.responses import RedirectResponse
class Utilities:
"""API Utilities"""
def __init__(self, app: FastAPI, constants):
self.constants = constants
self.blocked_response_status = 422
self.blocked_response_content = None
self.blocked_redirect_uri = "https://codey.lol"
self.app = app
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)
return RedirectResponse(url=self.blocked_redirect_uri)
def get_no_endpoint_found(self, path: str | None = None): # pylint: disable=unused-argument
"""Get 404 Response"""
@ -29,27 +28,21 @@ class Utilities:
Accepts path as an argument to allow fine tuning access for each API key, not currently in use.
"""
# print(f"Testing with path: {path}, key: {key}")
if not key or not key.startswith("Bearer "):
return False
key = key.split("Bearer ", maxsplit=1)[1].strip()
if not key in self.constants.API_KEYS:
# print("Auth failed")
return False
if req_type == 2:
if not key.startswith("PRV-"):
# print("Auth failed - not a PRV key")
return False
else:
return True
if path.lower().startswith("/xc/") and not key.startswith("XC-"):
# print("Auth failed - not an XC Key")
return False
# print("Auth succeeded")
return True