playlists have been stored to redis for faster retrieval; additional work needed (playlist management, typeahead, etc- to move away from SQLite)

This commit is contained in:
2025-07-20 15:50:25 -04:00
parent c42ebbfe53
commit 8603b11438
4 changed files with 79 additions and 81 deletions

5
.gitignore vendored
View File

@ -18,4 +18,7 @@ mypy.ini
.python-version .python-version
get_next_track.py get_next_track.py
endpoints/radio.py endpoints/radio.py
utils/radio_util.py utils/radio_util.py
redis_playlist.py
endpoints/radio2
endpoints/radio2/**

View File

@ -97,7 +97,6 @@ routes: dict = {
"radio": importlib.import_module("endpoints.radio").Radio( "radio": importlib.import_module("endpoints.radio").Radio(
app, util, constants, loop app, util, constants, loop
), ),
"mgr": importlib.import_module("endpoints.mgr.mgr_test").Mgr(app, util, constants),
"meme": importlib.import_module("endpoints.meme").Meme(app, util, constants), "meme": importlib.import_module("endpoints.meme").Meme(app, util, constants),
} }

View File

@ -23,7 +23,6 @@ from fastapi import (
from fastapi_throttle import RateLimiter from fastapi_throttle import RateLimiter
from fastapi.responses import RedirectResponse, JSONResponse from fastapi.responses import RedirectResponse, JSONResponse
class Radio(FastAPI): class Radio(FastAPI):
"""Radio Endpoints""" """Radio Endpoints"""
@ -34,7 +33,6 @@ class Radio(FastAPI):
self.loop = loop self.loop = loop
self.radio_util = radio_util.RadioUtil(self.constants, self.loop) self.radio_util = radio_util.RadioUtil(self.constants, self.loop)
self.playlists_loaded: bool = False self.playlists_loaded: bool = False
self.endpoints: dict = { self.endpoints: dict = {
"radio/np": self.radio_now_playing, "radio/np": self.radio_now_playing,
"radio/request": self.radio_request, "radio/request": self.radio_request,
@ -67,7 +65,7 @@ class Radio(FastAPI):
async def on_start(self) -> None: async def on_start(self) -> None:
stations = ", ".join(self.radio_util.db_queries.keys()) stations = ", ".join(self.radio_util.db_queries.keys())
logging.info("radio: Initializing stations:\n%s", stations) logging.info("radio: Initializing stations:\n%s", stations)
self.loop.run_in_executor(None, self.radio_util.load_playlists) await self.radio_util.load_playlists()
async def radio_skip( async def radio_skip(
self, data: ValidRadioNextRequest, request: Request self, data: ValidRadioNextRequest, request: Request
@ -300,6 +298,8 @@ class Radio(FastAPI):
- **station**: default: "main" - **station**: default: "main"
""" """
logging.info("Radio get next") logging.info("Radio get next")
if data.station not in self.radio_util.active_playlist.keys():
raise HTTPException(status_code=500, detail="No such station/not ready")
if not self.util.check_key(path=request.url.path, req_type=4, key=data.key): if not self.util.check_key(path=request.url.path, req_type=4, key=data.key):
raise HTTPException(status_code=403, detail="Unauthorized") raise HTTPException(status_code=403, detail="Unauthorized")
if ( if (

View File

@ -3,7 +3,6 @@ import traceback
import time import time
import datetime import datetime
import os import os
import random
from uuid import uuid4 as uuid from uuid import uuid4 as uuid
from typing import Union, Optional, Iterable from typing import Union, Optional, Iterable
from aiohttp import ClientSession, ClientTimeout from aiohttp import ClientSession, ClientTimeout
@ -14,6 +13,12 @@ import gpt
import music_tag # type: ignore import music_tag # type: ignore
from rapidfuzz import fuzz from rapidfuzz import fuzz
from endpoints.constructors import RadioException from endpoints.constructors import RadioException
import redis.asyncio as redis
from redis.commands.search.query import Query # noqa
from redis.commands.search.indexDefinition import IndexDefinition, IndexType # noqa
from redis.commands.search.field import TextField # noqa
from redis.commands.json.path import Path # noqa
from lyric_search.sources import private
double_space: Pattern = regex.compile(r"\s{2,}") double_space: Pattern = regex.compile(r"\s{2,}")
non_alnum: Pattern = regex.compile(r"[^a-zA-Z0-9]") non_alnum: Pattern = regex.compile(r"[^a-zA-Z0-9]")
@ -28,6 +33,7 @@ class RadioUtil:
self.loop = loop self.loop = loop
self.gpt = gpt.GPT(self.constants) self.gpt = gpt.GPT(self.constants)
self.ls_uri: str = self.constants.LS_URI self.ls_uri: str = self.constants.LS_URI
self.redis_client = redis.Redis(password=private.REDIS_PW)
self.sqlite_exts: list[str] = [ self.sqlite_exts: list[str] = [
"/home/kyle/api/solibs/spellfix1.cpython-311-x86_64-linux-gnu.so" "/home/kyle/api/solibs/spellfix1.cpython-311-x86_64-linux-gnu.so"
] ]
@ -63,10 +69,18 @@ class RadioUtil:
# # "pop punk", # # "pop punk",
# # "pop-punk", # # "pop-punk",
] ]
self.playlists: list = [
"main",
"rock",
"rap",
"electronic",
"classical",
"pop",
]
self.active_playlist: dict[str, list[dict]] = {} self.active_playlist: dict[str, list[dict]] = {}
self.playlists_loaded: bool = False self.playlists_loaded: bool = False
self.now_playing: dict[str, dict] = { self.now_playing: dict[str, dict] = {
k: { playlist: {
"artist": "N/A", "artist": "N/A",
"song": "N/A", "song": "N/A",
"album": "N/A", "album": "N/A",
@ -77,7 +91,7 @@ class RadioUtil:
"end": 0, "end": 0,
"file_path": None, "file_path": None,
"id": None, "id": None,
} for k in self.db_queries.keys() } for playlist in self.playlists
} }
self.webhooks: dict = { self.webhooks: dict = {
"gpt": { "gpt": {
@ -360,94 +374,76 @@ class RadioUtil:
traceback.print_exc() traceback.print_exc()
return "Not Found" return "Not Found"
def load_playlists(self) -> None: async def load_playlists(self) -> None:
"""Load Playlists""" """Load Playlists"""
try: try:
logging.info("Loading playlists...") logging.info("Loading playlists...")
if isinstance(self.active_playlist, dict): if isinstance(self.active_playlist, dict):
self.active_playlist.clear() self.active_playlist.clear()
with sqlite3.connect( for playlist in self.playlists:
f"file:{self.playback_db_path}?mode=ro", uri=True, timeout=30 playlist_redis_key: str = f"playlist:{playlist}"
) as db_conn: _playlist = await self.redis_client.json().get(playlist_redis_key)
db_conn.row_factory = sqlite3.Row if playlist not in self.active_playlist.keys():
for station in self.db_queries: self.active_playlist[playlist] = []
db_query = self.db_queries.get(station) self.active_playlist[playlist] = [
if not db_query: {
logging.critical("No query found for %s", station) "uuid": str(uuid().hex),
continue "id": r["id"],
if station not in self.active_playlist: "artist": double_space.sub(" ", r["artist"]).strip(),
self.active_playlist[station] = [] "song": double_space.sub(" ", r["song"]).strip(),
time_start = time.time() "album": double_space.sub(" ", r["album"]).strip(),
logging.info("[%s] Running query: %s", "genre": r["genre"] if r["genre"] else "Not Found",
time_start, db_query) "artistsong": double_space.sub(
db_cursor = db_conn.execute(db_query) " ", r["artistdashsong"]
results: list[sqlite3.Row] = db_cursor.fetchall() ).strip(),
time_end = time.time() "file_path": r["file_path"],
logging.info("[%s] Query completed; Time taken: %s", time_end, (time_end - time_start)) "duration": r["duration"],
self.active_playlist[station] = [ } for r in _playlist
{ if r not in self.active_playlist[playlist]
"uuid": str(uuid().hex), ]
"id": r["id"], logging.info(
"artist": double_space.sub(" ", r["artist"]).strip(),
"song": double_space.sub(" ", r["song"]).strip(),
"album": double_space.sub(" ", r["album"]).strip(),
"genre": r["genre"] if r["genre"] else "Not Found",
"artistsong": double_space.sub(
" ", r["artistdashsong"]
).strip(),
"file_path": r["file_path"],
"duration": r["duration"],
}
for r in results
if r not in self.active_playlist[station]
]
logging.info(
"Populated playlist: %s with %s items", "Populated playlist: %s with %s items",
station, len(self.active_playlist[station]), playlist, len(self.active_playlist[playlist]),
) )
if not station == "rock":# REMOVE ME, AFI RELATED """Dedupe"""
random.shuffle(self.active_playlist[station]) logging.info("Removing duplicate tracks...")
dedupe_processed = []
for item in self.active_playlist[playlist]:
artistsongabc: str = non_alnum.sub("", item.get("artistsong", None))
if not artistsongabc:
logging.info("Missing artistsong: %s", item)
continue
if artistsongabc in dedupe_processed:
self.active_playlist[playlist].remove(item)
dedupe_processed.append(artistsongabc)
"""Dedupe""" logging.info(
logging.info("Removing duplicate tracks...") "Duplicates for playlist: %s removed. New playlist size: %s",
dedupe_processed = [] playlist, len(self.active_playlist[playlist]),
for item in self.active_playlist[station]: )
artistsongabc: str = non_alnum.sub("", item.get("artistsong", None))
if not artistsongabc:
logging.info("Missing artistsong: %s", item)
continue
if artistsongabc in dedupe_processed:
self.active_playlist[station].remove(item)
dedupe_processed.append(artistsongabc)
if playlist == 'main' and self.playback_genres:
new_playlist: list[dict] = []
logging.info("Limiting playback genres")
for item in self.active_playlist[playlist]:
item_genres = item.get("genre", "").strip().lower()
# Check if any genre matches and item isn't already in new_playlist
if any(genre.strip().lower() in item_genres for genre in self.playback_genres):
if item not in new_playlist:
new_playlist.append(item)
self.active_playlist[playlist] = new_playlist
logging.info( logging.info(
"Duplicates for playlist: %s removed. New playlist size: %s", "%s items for playlist: %s remain for playback after filtering",
station, len(self.active_playlist[station]), playlist, len(self.active_playlist[playlist]),
) )
# logging.info( """Loading Complete"""
# "Playlist: %s", logging.info(f"Skipping: {playlist}")
# [str(a.get("artistsong", "")) for a in self.active_playlist[station]], await self._ls_skip(playlist) # Request skip from LS to bring streams current
# )
if station == 'main' and self.playback_genres:
new_playlist: list[dict] = []
logging.info("Limiting playback genres")
for item in self.active_playlist[station]:
item_genres = item.get("genre", "").strip().lower()
# Check if any genre matches and item isn't already in new_playlist
if any(genre.strip().lower() in item_genres for genre in self.playback_genres):
if item not in new_playlist:
new_playlist.append(item)
self.active_playlist[station] = new_playlist
logging.info(
"%s items for playlist: %s remain for playback after filtering",
station, len(self.active_playlist[station]),
)
self.playlists_loaded = True self.playlists_loaded = True
# self.loop.run_until_complete(self._ls_skip())
except Exception as e: except Exception as e:
logging.info("Playlist load failed: %s", str(e)) logging.info("Playlist load failed: %s", str(e))
traceback.print_exc() traceback.print_exc()