refactored, fixed mp3 length bug, added lossless tag to database

This commit is contained in:
Kristy Fournier 2025-10-17 15:32:19 -04:00
parent faac93b1f6
commit 6ece2d3ea1
4 changed files with 46 additions and 24 deletions

View file

@ -3,7 +3,8 @@ let ip;
let alertTime = 2; let alertTime = 2;
let adminPass = ""; let adminPass = "";
const ERR_NO_ADMIN = "401"; // gonna use this later to refactor const ERR_NO_ADMIN = "401"; // gonna use this later to refactor
const VALID_FILE_EXT = ["mp3","flac","wav"] const VALID_FILE_EXT = ["mp3","flac","wav"];
async function alertText(text="Song Added!") { async function alertText(text="Song Added!") {
alertbox = document.getElementById("alert"); alertbox = document.getElementById("alert");
alertbox.innerHTML = text; alertbox.innerHTML = text;
@ -17,6 +18,7 @@ async function alertText(text="Song Added!") {
async function getFromServer(bodyInfo, source="",password=adminPass) { async function getFromServer(bodyInfo, source="",password=adminPass) {
try{ try{
if (bodyInfo != null) { if (bodyInfo != null) {
// the currently set password is always included in every request
bodyInfo["password"] = password; bodyInfo["password"] = password;
} }
const response = await fetch("http://"+ip+"/"+source, { const response = await fetch("http://"+ip+"/"+source, {
@ -142,6 +144,7 @@ async function searchSongs(searchTerm){
document.getElementById("songlist").innerHTML = "<h1>We might not have that one...</h1>"; document.getElementById("songlist").innerHTML = "<h1>We might not have that one...</h1>";
} }
} }
function alertTimeEnter(e){ function alertTimeEnter(e){
if (e.key == "Enter") { if (e.key == "Enter") {
e.preventDefault(); e.preventDefault();
@ -178,6 +181,7 @@ function ipSetter(){
alertText("Your IP is now set to "+ipBox+" at port 19054 (Default)") alertText("Your IP is now set to "+ipBox+" at port 19054 (Default)")
} }
} }
// anytime the server ip changes the qrcode should change to use it
qrCodeGenerate() qrCodeGenerate()
} }

View file

@ -35,7 +35,7 @@ except:
songDatabase.execute("UPDATE meta SET data = ? WHERE id = 'songDirectory'", (soundLocation,)) songDatabase.execute("UPDATE meta SET data = ? WHERE id = 'songDirectory'", (soundLocation,))
if args.mode.lower() == "update": if args.mode.lower() == "update":
#Create if not exists #Create if not exists
songDatabase.execute("CREATE TABLE IF NOT EXISTS songs (filename TEXT PRIMARY KEY, title TEXT, artist TEXT, art TEXT, length INTEGER);") songDatabase.execute("CREATE TABLE IF NOT EXISTS songs (filename TEXT PRIMARY KEY, title TEXT, artist TEXT, art TEXT, length INTEGER, lossless INTEGER);")
songDatabase.execute("SELECT filename FROM songs;") songDatabase.execute("SELECT filename FROM songs;")
dBfilelist = songDatabase.fetchall() dBfilelist = songDatabase.fetchall()
dBfilelistSet = set() dBfilelistSet = set()
@ -50,7 +50,7 @@ if args.mode.lower() == "update":
print("new songs: " + ", ".join(songFiles)) print("new songs: " + ", ".join(songFiles))
elif args.mode.lower()=="new": elif args.mode.lower()=="new":
songDatabase.execute("DROP TABLE IF EXISTS songs;") songDatabase.execute("DROP TABLE IF EXISTS songs;")
songDatabase.execute("CREATE TABLE songs (filename TEXT PRIMARY KEY, title TEXT, artist TEXT, art TEXT, length INTEGER);") songDatabase.execute("CREATE TABLE songs (filename TEXT PRIMARY KEY, title TEXT, artist TEXT, art TEXT, length INTEGER, lossless INTEGER);")
else: else:
raise ValueError("Must be \"new\" or \"update\"") raise ValueError("Must be \"new\" or \"update\"")
@ -65,37 +65,43 @@ if args.art.lower() == "true" and not(args.apikey == ""):
validFormats = ["mp3","flac","wav"] validFormats = ["mp3","flac","wav"]
for i in songFiles: for i in songFiles:
# songFiles is the list of filenames, so i is the filename of each song
global song global song
extension = i.split(".") filenamesplit = i.split(".")
extension = extension[len(extension)-1] extension = filenamesplit[len(filenamesplit)-1]
lossless = 0 # sqlite doesn't have booleans. what is this, C?
if not(extension.lower() in validFormats): if not(extension.lower() in validFormats):
# skip any non music files (like directories or cover art) # skip any non music files (like directories or cover art)
continue continue
try: try:
print(extension)
# get the metadata
if(extension.lower() == "mp3"): if(extension.lower() == "mp3"):
# get the metadata
song = EasyID3(soundLocation+i) song = EasyID3(soundLocation+i)
elif(extension.lower() == "flac"): elif(extension.lower() == "flac"):
song = mutagen.flac.FLAC(soundLocation+i) song = mutagen.flac.FLAC(soundLocation+i)
lossless = 1
elif(extension.lower() in ["wav","wave"]): elif(extension.lower() in ["wav","wave"]):
# Im actually pretty sure waves can't have metadata, but whatevz
song = mutagen.wave.WAVE(soundLocation+i) song = mutagen.wave.WAVE(soundLocation+i)
lossless = 1
title = song['title'][0] title = song['title'][0]
artist = song['artist'][0] artist = song['artist'][0]
except: except:
if "_" in i: if "_" in i:
# if metadata is missing, try to use file name following title_artist.mp3 # if metadata is missing, try to use file name following "title_artist.mp3"
song = i.split("_") song = i.split("_")
title = song[0] title = song[0]
artist = song[1].split(".")[0] artist = song[1].split(".")[0]
elif "-" in i: elif "-" in i:
# if there's no underscore, try artist - title.mp3 # if there's no underscore, try "artist - title.mp3"
song = i.split("-") song = i.split("-")
title = song[1].split(".")[0] title = song[1].split(".")[0]
artist = song[0] artist = song[0]
title = title.strip() title = title.strip()
artist = artist.strip() artist = artist.strip()
else: else:
#if the file is not formatted with an underscore, the title is the file name #if the file is not formatted with an underscore or hyphen, the title is the file name
title = i title = i
artist = None artist = None
if args.art.lower() == "true" and not(args.apikey == ""): if args.art.lower() == "true" and not(args.apikey == ""):
@ -112,13 +118,19 @@ for i in songFiles:
else: else:
image=None image=None
try: try:
length = math.ceil(song.info.length) if extension.lower() in ['flac','wave','wav']:
length = math.ceil(song.info.length)
elif extension.lower() == "mp3":
# for some reason ID3 and mutagen.mp3 get different info
# artist and title are in id3() and length is in mp3()
# I dunno why
length = MP3(soundLocation+i).info.length
except: except:
length = 0 length = 0
if len(songFiles) != 1: if len(songFiles) != 1:
index = (songFiles.index(i))%4 index = (songFiles.index(i))%4
print("\r" + str(loading[index] + str(math.floor((songFiles.index(i)/(len(songFiles)-1))*100))+ "%"), end='', flush=True) print("\r" + str(loading[index] + str(math.floor((songFiles.index(i)/(len(songFiles)-1))*100))+ "%"), end='', flush=True)
# each "song" is stored as a SQLite entry following the format seen in the readME # each "song" is stored as a SQLite entry following the format seen below
songDatabase.execute(f"INSERT INTO songs (filename, title, artist, art, length) VALUES (?,?,?,?,?)",(i,title,artist,image,length)) songDatabase.execute(f"INSERT INTO songs (filename, title, artist, art, length, lossless) VALUES (?,?,?,?,?,?)",(i,title,artist,image,length,lossless))
fileOfDB.commit() fileOfDB.commit()

View file

@ -40,11 +40,13 @@ elif "/" in soundLocation:
else: else:
soundLocation += "\\" soundLocation += "\\"
#Create Virtual table for searching #Create Virtual table for searching
#I'm not sure why i don't do this in the databaseGenerator, but it also takes like 3 seconds so i'm not messing with it rn
songDatabase.execute("DROP TABLE virtualSongs;") songDatabase.execute("DROP TABLE virtualSongs;")
songDatabase.execute("CREATE VIRTUAL TABLE virtualSongs USING fts5(filename, title, artist, art, length);") songDatabase.execute("CREATE VIRTUAL TABLE virtualSongs USING fts5(filename, title, artist, art, length, lossless);")
songDatabase.execute("INSERT INTO virtualSongs SELECT * FROM songs;") songDatabase.execute("INSERT INTO virtualSongs SELECT * FROM songs;")
fileofDB.commit() fileofDB.commit()
fileofDB.close() fileofDB.close()
#Initializing all the global stuff #Initializing all the global stuff
random.seed() random.seed()
global partyMode global partyMode
@ -66,6 +68,7 @@ CORS(app)
def queueSong(song): def queueSong(song):
with playlistLock: with playlistLock:
playlist.append(song) playlist.append(song)
# this is a loop that plays the songs and checks for playlist changes, skips, ect. # this is a loop that plays the songs and checks for playlist changes, skips, ect.
def playQueuedSongs(): def playQueuedSongs():
global skipNow global skipNow
@ -94,15 +97,11 @@ def playQueuedSongs():
songDatabase.execute("SELECT * FROM songs ORDER BY RANDOM() LIMIT 1;") songDatabase.execute("SELECT * FROM songs ORDER BY RANDOM() LIMIT 1;")
result = songDatabase.fetchall() result = songDatabase.fetchall()
# adds the random songs for party mode # adds the random songs for party mode
# the above 2 means this only applies if (a song is playing (or paused)) and the queue is empty # the above 2 means this only applies if (a song is playing or paused) and (the queue is empty)
playlist.append(result[0][0]) playlist.append(result[0][0])
# check for new songs every second # check for new songs every second
# I just didn't want to eat too much processing looping # I just didn't want to eat too much processing looping
time.sleep(1) time.sleep(1)
# start the media player thread
queueThread = threading.Thread(target=playQueuedSongs)
queueThread.daemon = True
queueThread.start()
@app.route("/controls", methods=['POST']) @app.route("/controls", methods=['POST'])
def playerControls(): def playerControls():
@ -112,13 +111,13 @@ def playerControls():
recieveData=request.get_json(force=True) recieveData=request.get_json(force=True)
if recieveData["control"] != None: if recieveData["control"] != None:
if recieveData["control"] == "play-pause": if recieveData["control"] == "play-pause":
if ADMIN_PASS == recieveData['password'] or not(ADMIN_PASS) or controlPerms["PP"]: if ADMIN_PASS == recieveData['password'] or controlPerms["PP"]:
player.pause() player.pause()
return "200" return "200"
else: else:
return ERR_NO_ADMIN return ERR_NO_ADMIN
elif recieveData["control"] == "skip": elif recieveData["control"] == "skip":
if ADMIN_PASS == recieveData['password'] or not(ADMIN_PASS) or controlPerms["SK"]: if ADMIN_PASS == recieveData['password'] or controlPerms["SK"]:
skipNow = True skipNow = True
return "200" return "200"
else: else:
@ -136,13 +135,13 @@ def settingsControl():
global player global player
recieveData = request.get_json(force=True) recieveData = request.get_json(force=True)
if recieveData["setting"] == "volume": if recieveData["setting"] == "volume":
if ADMIN_PASS == recieveData['password'] or not(ADMIN_PASS) or controlPerms["VOL"]: if ADMIN_PASS == recieveData['password'] or controlPerms["VOL"]:
volumePassed = player.audio_set_volume(int(recieveData["level"])) volumePassed = player.audio_set_volume(int(recieveData["level"]))
return {"volumePassed":volumePassed} return {"volumePassed":volumePassed}
else: else:
return ERR_NO_ADMIN return ERR_NO_ADMIN
elif recieveData["setting"] == "partymode-toggle": elif recieveData["setting"] == "partymode-toggle":
if ADMIN_PASS == recieveData['password'] or not(ADMIN_PASS) or controlPerms["PM"]: if ADMIN_PASS == recieveData['password'] or controlPerms["PM"]:
partyMode = not(partyMode) partyMode = not(partyMode)
return "200" return "200"
else: else:
@ -231,5 +230,11 @@ def getPlaylist():
return tempPlaylist return tempPlaylist
if __name__ == "__main__": if __name__ == "__main__":
# There's not really a whole lot of point to a main function for something like this, you'd never use any of these methods
# elsewhere, but its just good practice i guess
# start the media player thread
queueThread = threading.Thread(target=playQueuedSongs)
queueThread.daemon = True
queueThread.start()
app.run(host='0.0.0.0', port=portTheUserPicked) app.run(host='0.0.0.0', port=portTheUserPicked)

View file

@ -65,7 +65,7 @@ These are specific details on each section of the app, and how to use them
- The total set of features that can be restricted is - The total set of features that can be restricted is
- Skip track - Skip track
- Play-pause toggle - Play-pause toggle
- Add track - Add track to queue
- Partymode toggle - Partymode toggle
- Change volume - Change volume
- When this argument is left out (or empty string) the admin features aren't used, and everyone can do everything - When this argument is left out (or empty string) the admin features aren't used, and everyone can do everything
@ -90,4 +90,5 @@ From left to right:
## External Credits ## External Credits
- QR Code Generator: JS file found [here](https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js) - QR Code Generator: JS file found [here](https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js)
- Cookie Popup: JS file found [here](https://cookieconsent.popupsmart.com/src/js/popper.js) - Cookie Popup: JS file found [here](https://cookieconsent.popupsmart.com/src/js/popper.js)
*See `LICENSE.md` for redistribution details.
*See `LICENSE.md` for redistribution and editing details.*