From f946a6f81c89c6064bbb1fddbb717292fcd4adfd Mon Sep 17 00:00:00 2001 From: codey Date: Tue, 22 Apr 2025 07:52:39 -0400 Subject: [PATCH] radio_util: db restructuring related changes, misc refactoring, add todos --- utils/radio_util.py | 210 +++++++++++++++++++++++++------------------- 1 file changed, 119 insertions(+), 91 deletions(-) diff --git a/utils/radio_util.py b/utils/radio_util.py index d215927..a4f4fe2 100644 --- a/utils/radio_util.py +++ b/utils/radio_util.py @@ -1,19 +1,28 @@ import logging import traceback import time -import regex -from regex import Pattern import datetime import os -import gpt -from aiohttp import ClientSession, ClientTimeout -import aiosqlite as sqlite3 -from typing import Union, Optional, LiteralString, Iterable from uuid import uuid4 as uuid +from typing import Union, Optional, Iterable +from aiohttp import ClientSession, ClientTimeout +import regex +from regex import Pattern +import aiosqlite as sqlite3 +import gpt from endpoints.constructors import RadioException double_space: Pattern = regex.compile(r"\s{2,}") +""" +TODO: + - Album art rework + - Allow tracks to be queried again based on genre; unable to query tracks based on genre presently, + as genre was moved outside track_file_map to artist_genre_map + - Ask GPT when we encounter an untagged (no genre defined) artist, automation is needed for this tedious task + - etc.. +""" + class RadioUtil: """ @@ -27,9 +36,12 @@ class RadioUtil: self.sqlite_exts: list[str] = [ "/home/api/api/solibs/spellfix1.cpython-311-x86_64-linux-gnu.so" ] - self.active_playlist_path: Union[str, LiteralString] = os.path.join( + self.active_playlist_path: str = os.path.join( "/usr/local/share", "sqlite_dbs", "track_file_map.db" ) + self.artist_genre_db_path: str = os.path.join( + "/usr/local/share", "sqlite_dbs", "artist_genre_map.db" + ) self.active_playlist_name = "default" # not used self.active_playlist: list[dict] = [] self.now_playing: dict = { @@ -64,6 +76,13 @@ class RadioUtil: return str(datetime.timedelta(seconds=s)).split(".", maxsplit=1)[0] async def trackdb_typeahead(self, query: str) -> Optional[list[str]]: + """ + Query track db for typeahead + Args: + query (str): The search query + Returns: + Optional[list[str]] + """ if not query: return None async with sqlite3.connect(self.active_playlist_path, timeout=1) as _db: @@ -126,7 +145,7 @@ class RadioUtil: result: Optional[sqlite3.Row | bool] = await db_cursor.fetchone() if not result or not isinstance(result, sqlite3.Row): return False - pushObj: dict = { + push_obj: dict = { "id": result["id"], "uuid": str(uuid().hex), "artist": result["artist"].strip(), @@ -136,17 +155,43 @@ class RadioUtil: "file_path": result["file_path"], "duration": result["duration"], } - self.active_playlist.insert(0, pushObj) + self.active_playlist.insert(0, push_obj) return True except Exception as e: logging.critical("search_playlist:: Search error occurred: %s", str(e)) traceback.print_exc() return False + async def get_genre(self, artist: str) -> str: + """ + Retrieve Genre for given Artist + Args: + artist (str): The artist to query + Returns: + str + """ + try: + artist = artist.strip() + query = "SELECT genre FROM artist_genre WHERE artist LIKE ?" + params = (f"%{artist}",) + async with sqlite3.connect(self.artist_genre_db_path, timeout=2) as _db: + _db.row_factory = sqlite3.Row + async with await _db.execute(query, params) as _cursor: + res = await _cursor.fetchone() + if not res: + raise RadioException( + f"Could not locate {artist} in artist_genre_map db." + ) + return res["genre"] + except Exception as e: + logging.info("Failed to look up genre for artist: %s (%s)", artist, str(e)) + traceback.print_exc() + return "Not Found" + async def load_playlist(self) -> None: """Load Playlist""" try: - logging.info(f"Loading playlist...") + logging.info("Loading playlist...") self.active_playlist.clear() # db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\ # GROUP BY artistdashsong ORDER BY RANDOM()' @@ -155,36 +200,10 @@ class RadioUtil: LIMITED GENRES """ - # db_query: str = """SELECT distinct(LOWER(TRIM(artist)) || " - " || LOWER(TRIM(song))), (TRIM(artist) || " - " || TRIM(song)) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\ - # WHERE (genre LIKE "%metalcore%"\ - # OR genre LIKE "%math rock%"\ - # OR genre LIKE "%punk rock%"\ - # OR genre LIKE "%metal%"\ - # OR genre LIKE "%punk%"\ - # OR genre LIKE "%electronic%"\ - # OR genre LIKE "%nu metal%"\ - # OR genre LIKE "%EDM%"\ - # OR genre LIKE "%post-hardcore%"\ - # OR genre LIKE "%pop rock%"\ - # OR genre LIKE "%experimental%"\ - # OR genre LIKE "%post-punk%"\ - # OR genre LIKE "%death metal%"\ - # OR genre LIKE "%electronicore%"\ - # OR genre LIKE "%hard rock%"\ - # OR genre LIKE "%psychedelic rock%"\ - # OR genre LIKE "%grunge%"\ - # OR genre LIKE "%house%"\ - # OR genre LIKE "%dubstep%"\ - # OR genre LIKE "%hardcore%"\ - # OR genre LIKE "%hair metal%"\ - # OR genre LIKE "%horror punk%"\ - # OR genre LIKE "%breakcore%"\ - # OR genre LIKE "%post-rock%"\ - # OR genre LIKE "%deathcore%"\ - # OR genre LIKE "%hardcore punk%"\ - # OR genre LIKE "%indie pop%"\ - # OR genre LIKE "%dnb%")\ - # GROUP BY artistdashsong ORDER BY RANDOM()""" + db_query: str = ( + 'SELECT distinct(LOWER(TRIM(artist)) || " - " || LOWER(TRIM(song))), (TRIM(artist) || " - " || TRIM(song))' + "AS artistdashsong, id, artist, song, album, file_path, duration FROM tracks GROUP BY artistdashsong ORDER BY RANDOM()" + ) """ LIMITED TO ONE/SMALL SUBSET OF GENRES @@ -200,14 +219,14 @@ class RadioUtil: # db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\ # WHERE (artist LIKE "%rise against%" OR artist LIKE "%i prevail%" OR artist LIKE "%volumes%" OR artist LIKE "%movements%" OR artist LIKE "%woe%" OR artist LIKE "%smittyztop%" OR artist LIKE "%chunk! no,%" OR artist LIKE "%fame on fire%" OR artist LIKE "%our last night%" OR artist LIKE "%animal in me%") AND (NOT song LIKE "%%stripped%%" AND NOT song LIKE "%(2022)%" AND NOT song LIKE "%(live%%" AND NOT song LIKE "%%acoustic%%" AND NOT song LIKE "%%instrumental%%" AND NOT song LIKE "%%remix%%" AND NOT song LIKE "%%reimagined%%" AND NOT song LIKE "%%alternative%%" AND NOT song LIKE "%%unzipped%%") GROUP BY artistdashsong ORDER BY RANDOM()'# ORDER BY album ASC, id ASC' - db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\ - WHERE (artist LIKE "%sullivan king%" OR artist LIKE "%kayzo%" OR artist LIKE "%adventure club%") AND (NOT song LIKE "%%stripped%%" AND NOT song LIKE "%(2022)%" AND NOT song LIKE "%(live%%" AND NOT song LIKE "%%acoustic%%" AND NOT song LIKE "%%instrumental%%" AND NOT song LIKE "%%remix%%" AND NOT song LIKE "%%reimagined%%" AND NOT song LIKE "%%alternative%%" AND NOT song LIKE "%%unzipped%%") GROUP BY artistdashsong ORDER BY RANDOM()'# ORDER BY album ASC, id ASC' + # db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\ + # WHERE (artist LIKE "%sullivan king%" OR artist LIKE "%kayzo%" OR artist LIKE "%adventure club%") AND (NOT song LIKE "%%stripped%%" AND NOT song LIKE "%(2022)%" AND NOT song LIKE "%(live%%" AND NOT song LIKE "%%acoustic%%" AND NOT song LIKE "%%instrumental%%" AND NOT song LIKE "%%remix%%" AND NOT song LIKE "%%reimagined%%" AND NOT song LIKE "%%alternative%%" AND NOT song LIKE "%%unzipped%%") GROUP BY artistdashsong ORDER BY RANDOM()'# ORDER BY album ASC, id ASC' # db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\ # WHERE (artist LIKE "%akira the don%") AND (NOT song LIKE "%%stripped%%" AND NOT song LIKE "%(2022)%" AND NOT song LIKE "%(live%%" AND NOT song LIKE "%%acoustic%%" AND NOT song LIKE "%%instrumental%%" AND NOT song LIKE "%%remix%%" AND NOT song LIKE "%%reimagined%%" AND NOT song LIKE "%%alternative%%" AND NOT song LIKE "%%unzipped%%") GROUP BY artistdashsong ORDER BY RANDOM()'# ORDER BY album ASC, id ASC' async with sqlite3.connect( - f"file:{self.active_playlist_path}?mode=ro", uri=True, timeout=2 + f"file:{self.active_playlist_path}?mode=ro", uri=True, timeout=15 ) as db_conn: db_conn.row_factory = sqlite3.Row async with await db_conn.execute(db_query) as db_cursor: @@ -219,7 +238,9 @@ class RadioUtil: "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 "Unknown", + "genre": await self.get_genre( + double_space.sub(" ", r["artist"]).strip() + ), "artistsong": double_space.sub( " ", r["artistdashsong"] ).strip(), @@ -232,7 +253,8 @@ class RadioUtil: "Populated active playlists with %s items", len(self.active_playlist), ) - except: + except Exception as e: + logging.info("Playlist load failed: %s", str(e)) traceback.print_exc() async def cache_album_art(self, track_id: int, album_art: bytes) -> None: @@ -244,18 +266,19 @@ class RadioUtil: Returns: None """ - try: - async with sqlite3.connect(self.active_playlist_path, timeout=2) as db_conn: - async with await db_conn.execute( - "UPDATE tracks SET album_art = ? WHERE id = ?", - ( - album_art, - track_id, - ), - ) as db_cursor: - await db_conn.commit() - except: - traceback.print_exc() + return None # TODO: Album art is being reworked, temporarily return None + # try: + # async with sqlite3.connect(self.active_playlist_path, timeout=2) as db_conn: + # async with await db_conn.execute( + # "UPDATE tracks SET album_art = ? WHERE id = ?", + # ( + # album_art, + # track_id, + # ), + # ) as db_cursor: + # await db_conn.commit() + # except: + # traceback.print_exc() async def get_album_art( self, track_id: Optional[int] = None, file_path: Optional[str] = None @@ -268,28 +291,29 @@ class RadioUtil: Returns: bytes """ - try: - async with sqlite3.connect(self.active_playlist_path, timeout=2) as db_conn: - db_conn.row_factory = sqlite3.Row - query: str = "SELECT album_art FROM tracks WHERE id = ?" - query_params: tuple = (track_id,) + return None # TODO: Album art is being reworked, temporarily return None + # try: + # async with sqlite3.connect(self.active_playlist_path, timeout=2) as db_conn: + # db_conn.row_factory = sqlite3.Row + # query: str = "SELECT album_art FROM tracks WHERE id = ?" + # query_params: tuple = (track_id,) - if file_path and not track_id: - query = "SELECT album_art FROM tracks WHERE file_path = ?" - query_params = (file_path,) + # if file_path and not track_id: + # query = "SELECT album_art FROM tracks WHERE file_path = ?" + # query_params = (file_path,) - async with await db_conn.execute(query, query_params) as db_cursor: - result: Optional[Union[sqlite3.Row, bool]] = ( - await db_cursor.fetchone() - ) - if not result or not isinstance(result, sqlite3.Row): - return None - return result["album_art"] - except: - traceback.print_exc() - return None + # async with await db_conn.execute(query, query_params) as db_cursor: + # result: Optional[Union[sqlite3.Row, bool]] = ( + # await db_cursor.fetchone() + # ) + # if not result or not isinstance(result, sqlite3.Row): + # return None + # return result["album_art"] + # except: + # traceback.print_exc() + # return None - def get_queue_item_by_uuid(self, uuid: str) -> Optional[tuple[int, dict]]: + def get_queue_item_by_uuid(self, _uuid: str) -> Optional[tuple[int, dict]]: """ Get queue item by UUID Args: @@ -298,7 +322,7 @@ class RadioUtil: Optional[tuple[int, dict]] """ for x, item in enumerate(self.active_playlist): - if item.get("uuid") == uuid: + if item.get("uuid") == _uuid: return (x, item) return None @@ -340,22 +364,25 @@ class RadioUtil: return response async def webhook_song_change(self, track: dict) -> None: + """ + Handles Song Change Outbounds (Webhooks) + Args: + track (dict) + Returns: + None + """ try: - """ - Handles Song Change Outbounds (Webhooks) - Args: - track (dict) - Returns: - None - """ - # First, send track info - friendly_track_start: str = time.strftime( - "%Y-%m-%d %H:%M:%S", time.localtime(track["start"]) - ) - friendly_track_end: str = time.strftime( - "%Y-%m-%d %H:%M:%S", time.localtime(track["end"]) - ) + """ + TODO: + Review friendly_track_start and friendly_track_end, not currently in use + """ + # friendly_track_start: str = time.strftime( + # "%Y-%m-%d %H:%M:%S", time.localtime(track["start"]) + # ) + # friendly_track_end: str = time.strftime( + # "%Y-%m-%d %H:%M:%S", time.localtime(track["end"]) + # ) hook_data: dict = { "username": "serious.FM", "embeds": [ @@ -443,4 +470,5 @@ class RadioUtil: ) as request: request.raise_for_status() except Exception as e: + logging.info("Webhook error occurred: %s", str(e)) traceback.print_exc()