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:
parent
e95ef3b088
commit
38dbddd297
22
base.py
22
base.py
@ -4,7 +4,7 @@ import importlib
|
|||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from lyric_search_new.sources import redis_cache
|
from lyric_search_new.sources import redis_cache
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ origins = [
|
|||||||
app.add_middleware(CORSMiddleware,
|
app.add_middleware(CORSMiddleware,
|
||||||
allow_origins=origins,
|
allow_origins=origins,
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["POST"],
|
allow_methods=["POST", "GET", "HEAD"],
|
||||||
allow_headers=["*"])
|
allow_headers=["*"])
|
||||||
|
|
||||||
|
|
||||||
@ -49,9 +49,18 @@ Blacklisted routes
|
|||||||
def disallow_get():
|
def disallow_get():
|
||||||
return util.get_blocked_response()
|
return util.get_blocked_response()
|
||||||
|
|
||||||
@app.get("/{any:path}")
|
@app.get("/{path}")
|
||||||
def disallow_get_any(var: Any = None): # pylint: disable=unused-argument
|
def disallow_get_any(request: Request, var: Any = None): # pylint: disable=unused-argument
|
||||||
return util.get_blocked_response()
|
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("/")
|
@app.post("/")
|
||||||
def disallow_base_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)
|
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: 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)
|
||||||
|
# Below: Misc endpoints
|
||||||
|
misc_endpoints = importlib.import_module("endpoints.misc").Misc(app, util, constants, glob_state)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
End Actionable Routes
|
End Actionable Routes
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
import regex
|
import regex
|
||||||
|
import json
|
||||||
|
|
||||||
from aiohttp import ClientSession, ClientTimeout
|
from aiohttp import ClientSession, ClientTimeout
|
||||||
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
|
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
|
||||||
@ -49,7 +50,8 @@ class AI(FastAPI):
|
|||||||
}
|
}
|
||||||
|
|
||||||
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"] if not endpoint == "testy" else ["POST", "GET"])
|
||||||
|
|
||||||
|
|
||||||
async def respond_via_webhook(self, data: ValidHookSongRequest, originalRequest: Request):
|
async def respond_via_webhook(self, data: ValidHookSongRequest, originalRequest: Request):
|
||||||
"""Respond via Webhook"""
|
"""Respond via Webhook"""
|
||||||
|
77
endpoints/misc.py
Normal file
77
endpoints/misc.py
Normal 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,
|
||||||
|
}
|
@ -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",
|
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:
|
timeout=ClientTimeout(connect=3, sock_read=8)) as request:
|
||||||
assert request.status in [200, 204]
|
assert request.status in [200, 204]
|
||||||
# return request.text
|
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
data = data.get('topalbums').get('album')
|
data = data.get('topalbums').get('album')
|
||||||
# print(f"Data:\n{data}")
|
|
||||||
retObj = [
|
retObj = [
|
||||||
{
|
{
|
||||||
'title': item.get('name')
|
'title': item.get('name')
|
||||||
} for item in data if not(item.get('name').lower() == "(null)") and int(item.get('playcount')) >= 50
|
} for item in data if not(item.get('name').lower() == "(null)") and int(item.get('playcount')) >= 50
|
||||||
]
|
]
|
||||||
# # print(f"Keys: {data[0].keys()}")
|
|
||||||
return retObj
|
return retObj
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
@ -50,7 +50,6 @@ class Cache:
|
|||||||
for res in redis_results:
|
for res in redis_results:
|
||||||
(key, row) = res
|
(key, row) = res
|
||||||
if key == matched_id:
|
if key == matched_id:
|
||||||
logging.info("Matched row: %s", row)
|
|
||||||
return LyricsResult(
|
return LyricsResult(
|
||||||
artist=row['artist'],
|
artist=row['artist'],
|
||||||
song=row['song'],
|
song=row['song'],
|
||||||
@ -114,6 +113,53 @@ class Cache:
|
|||||||
await self.redis_cache.redis_store(sqlite_insert_id, lyr_result)
|
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:
|
async def sqlite_store(self, lyr_result: LyricsResult) -> int:
|
||||||
"""
|
"""
|
||||||
Store lyrics to SQLite Cache
|
Store lyrics to SQLite Cache
|
||||||
@ -188,7 +234,7 @@ class Cache:
|
|||||||
|
|
||||||
"""Check Redis First"""
|
"""Check Redis First"""
|
||||||
|
|
||||||
logging.info("Checking redis cache for %s...",
|
logging.debug("Checking redis cache for %s...",
|
||||||
f"{artist} - {song}")
|
f"{artist} - {song}")
|
||||||
redis_result = await self.redis_cache.search(artist=artist,
|
redis_result = await self.redis_cache.search(artist=artist,
|
||||||
song=song)
|
song=song)
|
||||||
|
@ -105,7 +105,7 @@ class RedisCache:
|
|||||||
if not is_random_search:
|
if not is_random_search:
|
||||||
logging.debug("Redis: Searching normally first")
|
logging.debug("Redis: Searching normally first")
|
||||||
(artist, song) = self.sanitize_input(artist, song)
|
(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(
|
search_res = await self.redis_client.ft().search(Query(
|
||||||
f"@artist:{artist} @song:{song}"
|
f"@artist:{artist} @song:{song}"
|
||||||
))
|
))
|
||||||
|
13
util.py
13
util.py
@ -3,21 +3,20 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from fastapi import FastAPI, Response, HTTPException
|
from fastapi import FastAPI, Response, HTTPException
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
|
|
||||||
class Utilities:
|
class Utilities:
|
||||||
"""API Utilities"""
|
"""API Utilities"""
|
||||||
def __init__(self, app: FastAPI, constants):
|
def __init__(self, app: FastAPI, constants):
|
||||||
self.constants = constants
|
self.constants = constants
|
||||||
self.blocked_response_status = 422
|
self.blocked_redirect_uri = "https://codey.lol"
|
||||||
self.blocked_response_content = None
|
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
def get_blocked_response(self, path: str | None = None): # pylint: disable=unused-argument
|
def get_blocked_response(self, path: str | None = None): # pylint: disable=unused-argument
|
||||||
"""Get Blocked HTTP Response"""
|
"""Get Blocked HTTP Response"""
|
||||||
logging.error("Rejected request: Blocked")
|
logging.error("Rejected request: Blocked")
|
||||||
return Response(content=self.blocked_response_content,
|
return RedirectResponse(url=self.blocked_redirect_uri)
|
||||||
status_code=self.blocked_response_status)
|
|
||||||
|
|
||||||
def get_no_endpoint_found(self, path: str | None = None): # pylint: disable=unused-argument
|
def get_no_endpoint_found(self, path: str | None = None): # pylint: disable=unused-argument
|
||||||
"""Get 404 Response"""
|
"""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.
|
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 "):
|
if not key or not key.startswith("Bearer "):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
key = key.split("Bearer ", maxsplit=1)[1].strip()
|
key = key.split("Bearer ", maxsplit=1)[1].strip()
|
||||||
|
|
||||||
if not key in self.constants.API_KEYS:
|
if not key in self.constants.API_KEYS:
|
||||||
# print("Auth failed")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if req_type == 2:
|
if req_type == 2:
|
||||||
if not key.startswith("PRV-"):
|
if not key.startswith("PRV-"):
|
||||||
# print("Auth failed - not a PRV key")
|
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if path.lower().startswith("/xc/") and not key.startswith("XC-"):
|
if path.lower().startswith("/xc/") and not key.startswith("XC-"):
|
||||||
# print("Auth failed - not an XC Key")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# print("Auth succeeded")
|
|
||||||
return True
|
return True
|
Loading…
x
Reference in New Issue
Block a user