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:
5
.gitignore
vendored
5
.gitignore
vendored
@ -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/**
|
1
base.py
1
base.py
@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 (
|
||||||
|
@ -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()
|
||||||
|
Reference in New Issue
Block a user