diff --git a/base.py b/base.py index 7defb66..ce2238b 100644 --- a/base.py +++ b/base.py @@ -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 diff --git a/endpoints/ai.py b/endpoints/ai.py index 724197f..2650585 100644 --- a/endpoints/ai.py +++ b/endpoints/ai.py @@ -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""" diff --git a/endpoints/misc.py b/endpoints/misc.py new file mode 100644 index 0000000..5cebba3 --- /dev/null +++ b/endpoints/misc.py @@ -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, + } \ No newline at end of file diff --git a/lastfm_wrapper.py b/lastfm_wrapper.py index b05dc71..565a4e3 100644 --- a/lastfm_wrapper.py +++ b/lastfm_wrapper.py @@ -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() diff --git a/lyric_search_new/sources/cache.py b/lyric_search_new/sources/cache.py index 9dc9386..d737ae1 100644 --- a/lyric_search_new/sources/cache.py +++ b/lyric_search_new/sources/cache.py @@ -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'], @@ -112,6 +111,53 @@ class Cache: sqlite_insert_id = await self.sqlite_store(lyr_result) if sqlite_insert_id: 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: @@ -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) diff --git a/lyric_search_new/sources/redis_cache.py b/lyric_search_new/sources/redis_cache.py index 0669dc9..b8d9a02 100644 --- a/lyric_search_new/sources/redis_cache.py +++ b/lyric_search_new/sources/redis_cache.py @@ -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}" )) diff --git a/util.py b/util.py index 70bae33..5ddac41 100644 --- a/util.py +++ b/util.py @@ -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 \ No newline at end of file