2025-02-10 20:29:57 -05:00
import logging
import traceback
import time
import datetime
2025-02-11 20:01:07 -05:00
import os
2025-04-26 21:27:55 -04:00
import random
2025-04-22 07:52:39 -04:00
from uuid import uuid4 as uuid
from typing import Union , Optional , Iterable
2025-02-11 11:26:20 -05:00
from aiohttp import ClientSession , ClientTimeout
2025-04-22 07:52:39 -04:00
import regex
from regex import Pattern
2025-04-26 12:01:45 -04:00
import sqlite3
2025-04-22 07:52:39 -04:00
import gpt
2025-04-22 09:18:15 -04:00
import music_tag # type: ignore
2025-04-26 17:17:42 -04:00
from rapidfuzz import fuzz
2025-03-04 08:11:55 -05:00
from endpoints . constructors import RadioException
2025-02-11 20:01:07 -05:00
2025-04-17 07:28:05 -04:00
double_space : Pattern = regex . compile ( r " \ s { 2,} " )
2025-04-26 17:17:42 -04:00
non_alnum : Pattern = regex . compile ( r " [^a-zA-Z0-9] " )
2025-04-17 07:28:05 -04:00
2025-04-22 07:52:39 -04:00
"""
TODO :
2025-04-22 08:14:51 -04:00
- get_genre should only be called once for load_playlist , rework get_genre to ( optionally ) accept a list of artists ,
2025-04-26 21:27:55 -04:00
and return ( optionally ) a list instead of an str
2025-04-22 07:52:39 -04:00
- Ask GPT when we encounter an untagged ( no genre defined ) artist , automation is needed for this tedious task
2025-04-26 21:27:55 -04:00
- etc . .
2025-04-22 07:52:39 -04:00
"""
2025-02-10 20:29:57 -05:00
class RadioUtil :
2025-02-15 21:09:33 -05:00
"""
Radio Utils
"""
2025-04-17 07:28:05 -04:00
2025-04-26 19:47:12 -04:00
def __init__ ( self , constants , loop ) - > None :
2025-02-10 20:29:57 -05:00
self . constants = constants
2025-04-26 19:47:12 -04:00
self . loop = loop
2025-02-10 20:29:57 -05:00
self . gpt = gpt . GPT ( self . constants )
2025-04-17 07:28:05 -04:00
self . ls_uri : str = self . constants . LS_URI
self . sqlite_exts : list [ str ] = [
" /home/api/api/solibs/spellfix1.cpython-311-x86_64-linux-gnu.so "
]
2025-04-22 07:52:39 -04:00
self . active_playlist_path : str = os . path . join (
2025-04-17 07:28:05 -04:00
" /usr/local/share " , " sqlite_dbs " , " track_file_map.db "
)
2025-04-22 07:52:39 -04:00
self . artist_genre_db_path : str = os . path . join (
" /usr/local/share " , " sqlite_dbs " , " artist_genre_map.db "
)
2025-04-22 09:18:15 -04:00
self . album_art_db_path : str = os . path . join (
" /usr/local/share " , " sqlite_dbs " , " track_album_art.db "
)
2025-04-22 15:31:26 -04:00
self . playback_genres : list [ str ] = [
2025-04-27 08:27:08 -04:00
# "post-hardcore",
# "post hardcore",
# "metalcore",
# "deathcore",
# "edm",
# "electronic",
2025-04-22 15:31:26 -04:00
]
2025-02-14 16:07:24 -05:00
self . active_playlist : list [ dict ] = [ ]
2025-04-22 16:24:00 -04:00
self . playlist_loaded : bool = False
2025-02-11 20:01:07 -05:00
self . now_playing : dict = {
2025-04-17 07:28:05 -04:00
" artist " : " N/A " ,
" song " : " N/A " ,
" album " : " N/A " ,
" genre " : " N/A " ,
" artistsong " : " N/A - N/A " ,
" duration " : 0 ,
" start " : 0 ,
" end " : 0 ,
" file_path " : None ,
" id " : None ,
}
2025-02-11 20:01:07 -05:00
self . webhooks : dict = {
2025-04-17 07:28:05 -04:00
" gpt " : {
" hook " : self . constants . GPT_WEBHOOK ,
} ,
" sfm " : {
" hook " : self . constants . SFM_WEBHOOK ,
} ,
}
def duration_conv ( self , s : Union [ int , float ] ) - > str :
2025-02-10 20:29:57 -05:00
"""
Convert duration given in seconds to hours , minutes , and seconds ( h : m : s )
Args :
2025-02-15 21:09:33 -05:00
s ( Union [ int , float ] ) : seconds to convert
2025-02-10 20:29:57 -05:00
Returns :
str
"""
2025-04-17 07:28:05 -04:00
return str ( datetime . timedelta ( seconds = s ) ) . split ( " . " , maxsplit = 1 ) [ 0 ]
2025-04-26 21:27:55 -04:00
def trackdb_typeahead ( self , query : str ) - > Optional [ list [ str ] ] :
2025-04-22 07:52:39 -04:00
"""
Query track db for typeahead
Args :
query ( str ) : The search query
Returns :
Optional [ list [ str ] ]
"""
2025-02-16 13:54:28 -05:00
if not query :
return None
2025-04-26 12:01:45 -04:00
with sqlite3 . connect ( self . active_playlist_path , timeout = 1 ) as _db :
2025-02-16 13:54:28 -05:00
_db . row_factory = sqlite3 . Row
2025-02-18 06:55:47 -05:00
db_query : str = """ SELECT DISTINCT(LOWER(TRIM(artist) || " - " || TRIM(song))), \
( TRIM ( artist ) | | " - " | | TRIM ( song ) ) as artistsong FROM tracks WHERE \
artistsong LIKE ? LIMIT 30 """
2025-02-16 13:54:28 -05:00
db_params : tuple [ str ] = ( f " % { query } % " , )
2025-04-26 12:01:45 -04:00
_cursor = _db . execute ( db_query , db_params )
result : Iterable [ sqlite3 . Row ] = _cursor . fetchall ( )
out_result = [ str ( r [ " artistsong " ] ) for r in result ]
return out_result
2025-04-17 07:28:05 -04:00
2025-04-26 17:17:42 -04:00
def datatables_search ( self , filter : str ) - > Optional [ list [ dict ] ] :
""" DataTables Search
Args :
filter ( str ) : The filter query to fuzzy match with
Returns :
2025-04-27 08:27:08 -04:00
list [ dict ] : List of matching playlist items ( if any are found )
2025-04-26 17:17:42 -04:00
"""
filter = filter . strip ( ) . lower ( )
matched : list [ dict ] = [ ]
for item in self . active_playlist :
artist : str = item . get ( " artist " , None )
song : str = item . get ( " song " , None )
artistsong : str = item . get ( " artistsong " , None )
album : str = item . get ( " album " , None )
if not artist or not song or not artistsong :
continue
if non_alnum . sub ( " " , filter ) in non_alnum . sub ( " " , artistsong ) . lower ( ) :
matched . append ( item )
continue
if (
fuzz . ratio ( filter , artist ) > = 85
or fuzz . ratio ( filter , song ) > = 85
or fuzz . ratio ( filter , album ) > = 85
) :
matched . append ( item )
return matched
def search_playlist (
2025-04-17 07:28:05 -04:00
self ,
artistsong : Optional [ str ] = None ,
artist : Optional [ str ] = None ,
song : Optional [ str ] = None ,
) - > bool :
2025-02-12 07:53:22 -05:00
"""
Search for track , add it up next in play queue if found
Args :
artistsong ( Optional [ str ] ) : Artist - Song combo to search [ ignored if artist / song are specified ]
artist ( Optional [ str ] ) : Artist to search ( ignored if artistsong is specified )
song ( Optional [ str ] ) : Song to search ( ignored if artistsong is specified )
Returns :
bool
"""
2025-02-11 20:01:07 -05:00
if artistsong and ( artist or song ) :
raise RadioException ( " Cannot search using combination provided " )
if not artistsong and ( not artist or not song ) :
raise RadioException ( " No query provided " )
try :
2025-04-26 19:47:12 -04:00
search_query : str = (
' SELECT id, artist, song, (artist || " - " || song) AS artistsong, album, file_path, duration FROM tracks \
2025-02-11 20:01:07 -05:00
WHERE editdist3 ( ( lower ( artist ) | | " " | | lower ( song ) ) , ( ? | | " " | | ? ) ) \
< = 410 ORDER BY editdist3 ( ( lower ( artist ) | | " " | | lower ( song ) ) , ? ) ASC LIMIT 1 '
2025-04-26 19:47:12 -04:00
)
2025-02-11 20:01:07 -05:00
if artistsong :
artistsong_split : list = artistsong . split ( " - " , maxsplit = 1 )
( search_artist , search_song ) = tuple ( artistsong_split )
else :
2025-02-14 16:07:24 -05:00
search_artist = artist
search_song = song
2025-02-11 20:01:07 -05:00
if not artistsong :
2025-02-14 16:07:24 -05:00
artistsong = f " { search_artist } - { search_song } "
2025-04-17 07:28:05 -04:00
search_params = (
search_artist . lower ( ) ,
search_song . lower ( ) ,
artistsong . lower ( ) ,
)
2025-04-26 12:01:45 -04:00
with sqlite3 . connect ( self . active_playlist_path , timeout = 2 ) as db_conn :
db_conn . enable_load_extension ( True )
2025-02-11 20:01:07 -05:00
for ext in self . sqlite_exts :
2025-04-26 12:01:45 -04:00
db_conn . load_extension ( ext )
2025-02-11 20:01:07 -05:00
db_conn . row_factory = sqlite3 . Row
2025-04-26 17:17:42 -04:00
db_cursor = db_conn . execute ( search_query , search_params )
2025-04-26 12:01:45 -04:00
result : Optional [ sqlite3 . Row | bool ] = db_cursor . fetchone ( )
if not result or not isinstance ( result , sqlite3 . Row ) :
return False
push_obj : dict = {
" id " : result [ " id " ] ,
" uuid " : str ( uuid ( ) . hex ) ,
" artist " : double_space . sub ( " " , result [ " artist " ] . strip ( ) ) ,
" song " : double_space . sub ( " " , result [ " song " ] . strip ( ) ) ,
" artistsong " : result [ " artistsong " ] . strip ( ) ,
" genre " : self . get_genre (
double_space . sub ( " " , result [ " artist " ] . strip ( ) )
) ,
" file_path " : result [ " file_path " ] ,
" duration " : result [ " duration " ] ,
}
self . active_playlist . insert ( 0 , push_obj )
return True
2025-02-11 20:01:07 -05:00
except Exception as e :
logging . critical ( " search_playlist:: Search error occurred: %s " , str ( e ) )
traceback . print_exc ( )
return False
2025-04-17 07:28:05 -04:00
2025-04-26 17:17:42 -04:00
def add_genre ( self , artist : str , genre : str ) - > bool :
2025-04-22 14:49:17 -04:00
"""
Add artist / genre pairing to DB
Args :
artist ( str )
genre ( str )
Returns :
bool
"""
try :
2025-04-26 12:01:45 -04:00
with sqlite3 . connect ( self . artist_genre_db_path , timeout = 2 ) as _db :
2025-04-22 15:31:26 -04:00
query : str = (
" INSERT OR IGNORE INTO artist_genre (artist, genre) VALUES(?, ?) "
)
2025-04-22 14:49:17 -04:00
params : tuple [ str , str ] = ( artist , genre )
2025-04-26 12:01:45 -04:00
res = _db . execute ( query , params )
if isinstance ( res . lastrowid , int ) :
2025-04-22 15:31:26 -04:00
logging . debug (
" Query executed successfully for %s / %s , committing " ,
artist ,
genre ,
)
2025-04-26 12:01:45 -04:00
_db . commit ( )
2025-04-22 14:49:17 -04:00
return True
2025-04-22 15:31:26 -04:00
logging . debug (
" Failed to store artist/genre pair: %s / %s (res: %s ) " , artist , genre , res
)
2025-04-22 14:49:17 -04:00
return False
except Exception as e :
2025-04-22 15:31:26 -04:00
logging . info (
" Failed to store artist/genre pair: %s / %s ( %s ) " , artist , genre , str ( e )
)
2025-04-22 14:49:17 -04:00
traceback . print_exc ( )
return False
2025-04-22 15:31:26 -04:00
2025-04-26 17:17:42 -04:00
def add_genres ( self , pairs : list [ dict [ str , str ] ] ) - > bool :
2025-04-22 15:04:46 -04:00
"""
( BATCH ) Add artist / genre pairings to DB
Expects list of dicts comprised of artist name ( key ) , genre ( value )
Args :
pairs ( list [ dict [ str , str ] ] ) : Pairs of artist / genres to add , list of dicts
Returns :
bool
"""
try :
added_rows : int = 0
2025-04-26 12:01:45 -04:00
with sqlite3 . connect ( self . artist_genre_db_path , timeout = 2 ) as _db :
2025-04-22 15:04:46 -04:00
for pair in pairs :
try :
artist , genre = pair
2025-04-26 19:47:12 -04:00
query : str = (
" INSERT OR IGNORE INTO artist_genre (artist, genre) VALUES(?, ?) "
)
2025-04-22 15:04:46 -04:00
params : tuple [ str , str ] = ( artist , genre )
2025-04-26 12:01:45 -04:00
res = _db . execute ( query , params )
if isinstance ( res . lastrowid , int ) :
2025-04-22 15:31:26 -04:00
logging . debug (
" add_genres: Query executed successfully for %s / %s " ,
artist ,
genre ,
)
2025-04-22 15:04:46 -04:00
added_rows + = 1
else :
2025-04-22 15:31:26 -04:00
logging . debug (
" Failed to store artist/genre pair: %s / %s (res: %s ) " ,
artist ,
genre ,
res ,
)
2025-04-22 15:04:46 -04:00
except Exception as e :
2025-04-22 15:31:26 -04:00
logging . info (
" Failed to store artist/genre pair: %s / %s ( %s ) " ,
artist ,
genre ,
str ( e ) ,
)
continue
2025-04-22 15:04:46 -04:00
if added_rows :
logging . info ( " add_genres: Committing %s rows " , added_rows )
2025-04-26 12:01:45 -04:00
_db . commit ( )
2025-04-22 15:04:46 -04:00
return True
logging . info ( " add_genres: Failed (No rows added) " )
return False
except Exception as e :
2025-04-22 15:31:26 -04:00
logging . info ( " Failed to store artist/genre pairs: %s " , str ( e ) )
2025-04-22 15:04:46 -04:00
traceback . print_exc ( )
return False
2025-04-22 14:49:17 -04:00
2025-04-26 12:01:45 -04:00
def get_genre ( self , artist : str ) - > str :
2025-04-22 07:52:39 -04:00
"""
Retrieve Genre for given Artist
Args :
artist ( str ) : The artist to query
Returns :
str
"""
try :
artist = artist . strip ( )
2025-04-22 15:49:32 -04:00
query : str = (
" SELECT genre FROM artist_genre WHERE artist LIKE ? COLLATE NOCASE "
)
2025-04-22 14:51:01 -04:00
params : tuple [ str ] = ( f " %% { artist } %% " , )
2025-04-26 12:01:45 -04:00
with sqlite3 . connect ( self . artist_genre_db_path , timeout = 2 ) as _db :
2025-04-22 07:52:39 -04:00
_db . row_factory = sqlite3 . Row
2025-04-26 12:01:45 -04:00
_cursor = _db . execute ( query , params )
res = _cursor . fetchone ( )
if not res :
return " Not Found " # Exception suppressed
# raise RadioException(
# f"Could not locate {artist} in artist_genre_map db."
# )
return res [ " genre " ]
2025-04-22 07:52:39 -04:00
except Exception as e :
logging . info ( " Failed to look up genre for artist: %s ( %s ) " , artist , str ( e ) )
traceback . print_exc ( )
return " Not Found "
2025-04-26 12:01:45 -04:00
def load_playlist ( self ) - > None :
2025-02-12 07:53:22 -05:00
""" Load Playlist """
2025-02-11 20:01:07 -05:00
try :
2025-04-22 07:52:39 -04:00
logging . info ( " Loading playlist... " )
2025-02-11 20:01:07 -05:00
self . active_playlist . clear ( )
2025-03-04 08:11:55 -05:00
# db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\
2025-02-11 20:01:07 -05:00
# GROUP BY artistdashsong ORDER BY RANDOM()'
2025-04-17 07:28:05 -04:00
2025-02-11 20:01:07 -05:00
"""
LIMITED GENRES
"""
2025-04-17 07:28:05 -04:00
2025-04-27 08:27:08 -04:00
# 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"
# )
2025-04-17 07:28:05 -04:00
2025-03-04 08:11:55 -05:00
"""
LIMITED TO ONE / SMALL SUBSET OF GENRES
"""
2025-04-17 07:28:05 -04:00
# db_query = 'SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, genre, file_path, duration FROM tracks\
# WHERE (artist LIKE "%winds of plague%" OR artist LIKE "%acacia st%" OR artist LIKE "%suicide si%" OR artist LIKE "%in dying%") AND (NOT song LIKE "%(live%") ORDER BY RANDOM()' #ORDER BY artist DESC, album ASC, song ASC'
2025-02-14 16:07:24 -05:00
"""
2025-03-04 08:11:55 -05:00
LIMITED TO ONE / SOME ARTISTS . . .
2025-02-14 16:07:24 -05:00
"""
2025-04-17 07:28:05 -04:00
2025-04-27 08:27:08 -04:00
db_query = ' SELECT distinct(artist || " - " || song) AS artistdashsong, id, artist, song, album, file_path, duration FROM tracks \
WHERE ( artist LIKE " %c hunk! % " ) 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 '
2025-04-17 07:28:05 -04:00
2025-04-22 07:52:39 -04:00
# 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'
2025-04-17 07:28:05 -04:00
# 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'
2025-04-26 12:01:45 -04:00
with sqlite3 . connect (
2025-04-22 07:52:39 -04:00
f " file: { self . active_playlist_path } ?mode=ro " , uri = True , timeout = 15
2025-04-17 07:28:05 -04:00
) as db_conn :
2025-02-11 20:01:07 -05:00
db_conn . row_factory = sqlite3 . Row
2025-04-26 12:01:45 -04:00
db_cursor = db_conn . execute ( db_query )
results : list [ sqlite3 . Row ] = db_cursor . fetchall ( )
self . active_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 " : self . get_genre (
double_space . sub ( " " , r [ " artist " ] ) . strip ( )
) ,
" artistsong " : double_space . sub (
" " , r [ " artistdashsong " ]
) . strip ( ) ,
" file_path " : r [ " file_path " ] ,
" duration " : r [ " duration " ] ,
}
2025-04-26 22:01:25 -04:00
for r in results
if r not in self . active_playlist
2025-04-26 12:01:45 -04:00
]
logging . info (
" Populated active playlists with %s items " ,
len ( self . active_playlist ) ,
)
2025-04-26 21:27:55 -04:00
random . shuffle ( self . active_playlist )
2025-04-26 22:01:25 -04:00
2025-04-26 21:27:55 -04:00
""" Dedupe """
logging . info ( " Removing duplicate tracks... " )
dedupe_processed = [ ]
for item in self . active_playlist :
2025-04-26 22:01:25 -04:00
artistsongabc : str = non_alnum . sub ( " " , item . get ( " artistsong " , None ) )
if not artistsongabc :
2025-04-26 21:27:55 -04:00
logging . info ( " Missing artistsong: %s " , item )
continue
if artistsongabc in dedupe_processed :
self . active_playlist . remove ( item )
dedupe_processed . append ( artistsongabc )
logging . info (
" Duplicates removed. " " New playlist size: %s " ,
2025-04-26 22:01:25 -04:00
len ( self . active_playlist ) ,
)
logging . info (
" Playlist: %s " ,
[ str ( a . get ( " artistsong " , " " ) ) for a in self . active_playlist ] ,
)
2025-04-26 21:27:55 -04:00
2025-04-26 12:01:45 -04:00
if self . playback_genres :
new_playlist : list [ dict ] = [ ]
logging . info ( " Limiting playback genres " )
for item in self . active_playlist :
matched_genre : bool = False
item_genres : str = item . get ( " genre " , " " ) . strip ( ) . lower ( )
for genre in self . playback_genres :
genre = genre . strip ( ) . lower ( )
if genre in item_genres :
2025-04-26 21:27:55 -04:00
if item in new_playlist :
continue
2025-04-26 12:01:45 -04:00
new_playlist . append ( item )
matched_genre = True
continue
if matched_genre :
continue
self . active_playlist = new_playlist
2025-04-17 07:28:05 -04:00
logging . info (
2025-04-26 12:01:45 -04:00
" %s items remain for playback after filtering " ,
2025-04-17 07:28:05 -04:00
len ( self . active_playlist ) ,
)
2025-04-26 12:01:45 -04:00
self . playlist_loaded = True
2025-04-26 19:59:38 -04:00
self . loop . run_until_complete ( self . _ls_skip ( ) )
2025-04-22 07:52:39 -04:00
except Exception as e :
logging . info ( " Playlist load failed: %s " , str ( e ) )
2025-02-11 20:01:07 -05:00
traceback . print_exc ( )
2025-04-17 07:28:05 -04:00
2025-04-26 19:47:12 -04:00
def cache_album_art ( self , track_id : int , file_path : str ) - > None :
2025-02-12 07:53:22 -05:00
"""
Cache Album Art to SQLite DB
Args :
track_id ( int ) : Track ID to update
2025-04-22 09:18:15 -04:00
file_path ( str ) : Path to file , for artwork extraction
2025-02-12 07:53:22 -05:00
Returns :
None
"""
2025-04-22 09:18:15 -04:00
try :
logging . info (
" cache_album_art: Attempting to store album art for track_id: %s " ,
track_id ,
)
tagger = music_tag . load_file ( file_path )
album_art = tagger [ " artwork " ] . first . data
2025-04-26 12:01:45 -04:00
with sqlite3 . connect ( self . album_art_db_path , timeout = 2 ) as db_conn :
db_cursor = db_conn . execute (
2025-04-22 09:18:15 -04:00
" INSERT OR IGNORE INTO album_art (track_id, album_art) VALUES(?, ?) " ,
(
track_id ,
album_art ,
) ,
2025-04-26 12:01:45 -04:00
)
if isinstance ( db_cursor . lastrowid , int ) :
db_conn . commit ( )
else :
2025-04-26 17:17:42 -04:00
logging . debug (
" No row inserted for track_id: %s w/ file_path: %s " ,
track_id ,
file_path ,
)
2025-04-26 12:01:45 -04:00
except Exception as e :
2025-04-26 17:17:42 -04:00
logging . debug ( " cache_album_art Exception: %s " , str ( e ) )
2025-04-22 09:18:15 -04:00
traceback . print_exc ( )
2025-04-17 07:28:05 -04:00
2025-04-26 19:47:12 -04:00
def get_album_art ( self , track_id : int ) - > Optional [ bytes ] :
2025-02-12 07:53:22 -05:00
"""
Get Album Art
Args :
2025-04-22 09:18:15 -04:00
track_id ( int ) : Track ID to query
2025-02-12 07:53:22 -05:00
Returns :
2025-04-22 09:18:15 -04:00
Optional [ bytes ]
2025-02-12 07:53:22 -05:00
"""
2025-04-22 09:18:15 -04:00
try :
2025-04-26 12:01:45 -04:00
with sqlite3 . connect ( self . album_art_db_path , timeout = 2 ) as db_conn :
2025-04-22 09:18:15 -04:00
db_conn . row_factory = sqlite3 . Row
query : str = " SELECT album_art FROM album_art WHERE track_id = ? "
2025-04-22 14:51:01 -04:00
query_params : tuple [ int ] = ( track_id , )
2025-04-22 07:52:39 -04:00
2025-04-26 12:01:45 -04:00
db_cursor = db_conn . execute ( query , query_params )
2025-04-26 17:17:42 -04:00
result : Optional [ Union [ sqlite3 . Row , bool ] ] = db_cursor . fetchone ( )
2025-04-26 12:01:45 -04:00
if not result or not isinstance ( result , sqlite3 . Row ) :
return None
return result [ " album_art " ]
except Exception as e :
2025-04-26 17:17:42 -04:00
logging . debug ( " get_album_art Exception: %s " , str ( e ) )
2025-04-22 09:18:15 -04:00
traceback . print_exc ( )
return None
2025-04-22 07:52:39 -04:00
def get_queue_item_by_uuid ( self , _uuid : str ) - > Optional [ tuple [ int , dict ] ] :
2025-02-11 20:01:07 -05:00
"""
Get queue item by UUID
Args :
uuid : The UUID to search
Returns :
2025-02-15 21:09:33 -05:00
Optional [ tuple [ int , dict ] ]
2025-02-11 20:01:07 -05:00
"""
for x , item in enumerate ( self . active_playlist ) :
2025-04-22 07:52:39 -04:00
if item . get ( " uuid " ) == _uuid :
2025-02-11 20:01:07 -05:00
return ( x , item )
return None
2025-04-17 07:28:05 -04:00
2025-02-11 20:01:07 -05:00
async def _ls_skip ( self ) - > bool :
2025-02-12 07:53:22 -05:00
"""
Ask LiquidSoap server to skip to the next track
Args :
None
Returns :
bool
"""
2025-02-11 20:01:07 -05:00
try :
async with ClientSession ( ) as session :
2025-04-17 07:28:05 -04:00
async with session . get (
f " { self . ls_uri } /next " , timeout = ClientTimeout ( connect = 2 , sock_read = 2 )
) as request :
request . raise_for_status ( )
text : Optional [ str ] = await request . text ( )
return text == " OK "
2025-02-11 20:01:07 -05:00
except Exception as e :
2025-04-17 07:28:05 -04:00
logging . debug ( " Skip failed: %s " , str ( e ) )
return False # failsafe
async def get_ai_song_info ( self , artist : str , song : str ) - > Optional [ str ] :
2025-02-10 20:29:57 -05:00
"""
Get AI Song Info
Args :
artist ( str )
song ( str )
Returns :
2025-02-15 21:09:33 -05:00
Optional [ str ]
2025-02-10 20:29:57 -05:00
"""
2025-02-15 21:09:33 -05:00
prompt : str = f " am going to listen to { song } by { artist } . "
response : Optional [ str ] = await self . gpt . get_completion ( prompt )
2025-02-10 20:29:57 -05:00
if not response :
logging . critical ( " No response received from GPT? " )
2025-02-14 16:07:24 -05:00
return None
2025-02-10 20:29:57 -05:00
return response
2025-04-17 07:28:05 -04:00
2025-02-10 20:29:57 -05:00
async def webhook_song_change ( self , track : dict ) - > None :
2025-04-22 07:52:39 -04:00
"""
Handles Song Change Outbounds ( Webhooks )
Args :
track ( dict )
Returns :
None
"""
2025-02-10 20:29:57 -05:00
try :
2025-04-22 07:52:39 -04:00
# First, send track info
2025-02-10 20:29:57 -05:00
"""
2025-04-22 07:52:39 -04:00
TODO :
Review friendly_track_start and friendly_track_end , not currently in use
2025-02-10 20:29:57 -05:00
"""
2025-04-22 07:52:39 -04:00
# 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"])
# )
2025-02-11 20:01:07 -05:00
hook_data : dict = {
2025-04-17 07:28:05 -04:00
" username " : " serious.FM " ,
" embeds " : [
{
2025-02-10 20:29:57 -05:00
" title " : " Now Playing " ,
2025-04-17 07:28:05 -04:00
" description " : f " ## { track [ ' song ' ] } \n by \n ## { track [ ' artist ' ] } " ,
" color " : 0x30C56F ,
2025-02-11 15:21:01 -05:00
" thumbnail " : {
2025-02-18 06:55:47 -05:00
" url " : f " https://api.codey.lol/radio/album_art?track_id= { track [ ' id ' ] } & { int ( time . time ( ) ) } " ,
2025-02-11 15:21:01 -05:00
} ,
2025-02-10 20:29:57 -05:00
" fields " : [
{
" name " : " Duration " ,
2025-04-17 07:28:05 -04:00
" value " : self . duration_conv ( track [ " duration " ] ) ,
2025-02-10 20:29:57 -05:00
" inline " : True ,
} ,
{
2025-02-11 16:44:53 -05:00
" name " : " Genre " ,
2025-04-17 07:28:05 -04:00
" value " : (
track [ " genre " ] if track [ " genre " ] else " Unknown "
) ,
2025-02-11 16:44:53 -05:00
" inline " : True ,
} ,
{
2025-02-10 20:29:57 -05:00
" name " : " Filetype " ,
2025-04-17 07:28:05 -04:00
" value " : track [ " file_path " ] . rsplit ( " . " , maxsplit = 1 ) [ 1 ] ,
2025-02-10 20:29:57 -05:00
" inline " : True ,
} ,
{
" name " : " Higher Res " ,
2025-03-10 10:00:23 -04:00
" value " : " [stream/icecast](https://stream.codey.lol/sfm.ogg) | [web player](https://codey.lol/radio) " ,
2025-02-10 20:29:57 -05:00
" inline " : True ,
2025-03-10 10:00:23 -04:00
} ,
{
" name " : " Album " ,
2025-04-17 07:28:05 -04:00
" value " : (
track [ " album " ] if track [ " album " ] else " Unknown "
) ,
2025-03-10 10:00:23 -04:00
} ,
2025-04-17 07:28:05 -04:00
] ,
}
] ,
}
sfm_hook : str = self . webhooks [ " sfm " ] . get ( " hook " )
2025-02-10 20:29:57 -05:00
async with ClientSession ( ) as session :
2025-04-17 07:28:05 -04:00
async with await session . post (
sfm_hook ,
json = hook_data ,
timeout = ClientTimeout ( connect = 5 , sock_read = 5 ) ,
headers = {
" content-type " : " application/json; charset=utf-8 " ,
} ,
) as request :
request . raise_for_status ( )
# Next, AI feedback
ai_response : Optional [ str ] = await self . get_ai_song_info (
track [ " artist " ] , track [ " song " ]
)
2025-02-11 20:01:07 -05:00
if not ai_response :
return
2025-04-17 07:28:05 -04:00
2025-02-14 16:07:24 -05:00
hook_data = {
2025-04-17 07:28:05 -04:00
" username " : " GPT " ,
" embeds " : [
{
2025-02-10 20:29:57 -05:00
" title " : " AI Feedback " ,
2025-04-17 07:28:05 -04:00
" color " : 0x35D0FF ,
2025-02-10 20:29:57 -05:00
" description " : ai_response . strip ( ) ,
2025-04-17 07:28:05 -04:00
}
] ,
}
ai_hook : str = self . webhooks [ " gpt " ] . get ( " hook " )
2025-02-10 20:29:57 -05:00
async with ClientSession ( ) as session :
2025-04-17 07:28:05 -04:00
async with await session . post (
ai_hook ,
json = hook_data ,
timeout = ClientTimeout ( connect = 5 , sock_read = 5 ) ,
headers = {
" content-type " : " application/json; charset=utf-8 " ,
} ,
) as request :
request . raise_for_status ( )
2025-02-10 20:29:57 -05:00
except Exception as e :
2025-04-22 07:52:39 -04:00
logging . info ( " Webhook error occurred: %s " , str ( e ) )
2025-02-10 20:29:57 -05:00
traceback . print_exc ( )