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
get_next_track.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(
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),
}

View File

@ -23,7 +23,6 @@ from fastapi import (
from fastapi_throttle import RateLimiter
from fastapi.responses import RedirectResponse, JSONResponse
class Radio(FastAPI):
"""Radio Endpoints"""
@ -34,7 +33,6 @@ class Radio(FastAPI):
self.loop = loop
self.radio_util = radio_util.RadioUtil(self.constants, self.loop)
self.playlists_loaded: bool = False
self.endpoints: dict = {
"radio/np": self.radio_now_playing,
"radio/request": self.radio_request,
@ -67,7 +65,7 @@ class Radio(FastAPI):
async def on_start(self) -> None:
stations = ", ".join(self.radio_util.db_queries.keys())
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(
self, data: ValidRadioNextRequest, request: Request
@ -300,6 +298,8 @@ class Radio(FastAPI):
- **station**: default: "main"
"""
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):
raise HTTPException(status_code=403, detail="Unauthorized")
if (

View File

@ -3,7 +3,6 @@ import traceback
import time
import datetime
import os
import random
from uuid import uuid4 as uuid
from typing import Union, Optional, Iterable
from aiohttp import ClientSession, ClientTimeout
@ -14,6 +13,12 @@ import gpt
import music_tag # type: ignore
from rapidfuzz import fuzz
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,}")
non_alnum: Pattern = regex.compile(r"[^a-zA-Z0-9]")
@ -28,6 +33,7 @@ class RadioUtil:
self.loop = loop
self.gpt = gpt.GPT(self.constants)
self.ls_uri: str = self.constants.LS_URI
self.redis_client = redis.Redis(password=private.REDIS_PW)
self.sqlite_exts: list[str] = [
"/home/kyle/api/solibs/spellfix1.cpython-311-x86_64-linux-gnu.so"
]
@ -63,10 +69,18 @@ class RadioUtil:
# # "pop punk",
# # "pop-punk",
]
self.playlists: list = [
"main",
"rock",
"rap",
"electronic",
"classical",
"pop",
]
self.active_playlist: dict[str, list[dict]] = {}
self.playlists_loaded: bool = False
self.now_playing: dict[str, dict] = {
k: {
playlist: {
"artist": "N/A",
"song": "N/A",
"album": "N/A",
@ -77,7 +91,7 @@ class RadioUtil:
"end": 0,
"file_path": None,
"id": None,
} for k in self.db_queries.keys()
} for playlist in self.playlists
}
self.webhooks: dict = {
"gpt": {
@ -360,94 +374,76 @@ class RadioUtil:
traceback.print_exc()
return "Not Found"
def load_playlists(self) -> None:
async def load_playlists(self) -> None:
"""Load Playlists"""
try:
logging.info("Loading playlists...")
if isinstance(self.active_playlist, dict):
self.active_playlist.clear()
with sqlite3.connect(
f"file:{self.playback_db_path}?mode=ro", uri=True, timeout=30
) as db_conn:
db_conn.row_factory = sqlite3.Row
for station in self.db_queries:
db_query = self.db_queries.get(station)
if not db_query:
logging.critical("No query found for %s", station)
continue
if station not in self.active_playlist:
self.active_playlist[station] = []
time_start = time.time()
logging.info("[%s] Running query: %s",
time_start, db_query)
db_cursor = db_conn.execute(db_query)
results: list[sqlite3.Row] = db_cursor.fetchall()
time_end = time.time()
logging.info("[%s] Query completed; Time taken: %s", time_end, (time_end - time_start))
self.active_playlist[station] = [
{
"uuid": str(uuid().hex),
"id": r["id"],
"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(
for playlist in self.playlists:
playlist_redis_key: str = f"playlist:{playlist}"
_playlist = await self.redis_client.json().get(playlist_redis_key)
if playlist not in self.active_playlist.keys():
self.active_playlist[playlist] = []
self.active_playlist[playlist] = [
{
"uuid": str(uuid().hex),
"id": r["id"],
"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 _playlist
if r not in self.active_playlist[playlist]
]
logging.info(
"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
random.shuffle(self.active_playlist[station])
"""Dedupe"""
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("Removing duplicate tracks...")
dedupe_processed = []
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)
logging.info(
"Duplicates for playlist: %s removed. New playlist size: %s",
playlist, len(self.active_playlist[playlist]),
)
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(
"Duplicates for playlist: %s removed. New playlist size: %s",
station, len(self.active_playlist[station]),
"%s items for playlist: %s remain for playback after filtering",
playlist, len(self.active_playlist[playlist]),
)
# logging.info(
# "Playlist: %s",
# [str(a.get("artistsong", "")) for a in self.active_playlist[station]],
# )
"""Loading Complete"""
logging.info(f"Skipping: {playlist}")
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.loop.run_until_complete(self._ls_skip())
except Exception as e:
logging.info("Playlist load failed: %s", str(e))
traceback.print_exc()