From 347feae50aa2e03e793cb3595f08a5b1b5a0a5fb Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Thu, 22 Jan 2026 18:57:03 -0500 Subject: [PATCH 1/7] added to requirements.txt, updated readme --- readme.md | 17 +++++++++-------- requirements.txt | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/readme.md b/readme.md index e1a753f..0bf9c42 100644 --- a/readme.md +++ b/readme.md @@ -26,7 +26,7 @@ webbyBits.py 2. Rename `example.env` to `.env` and... - Set the location where your audio files are (Default: `./sound/`) - Set the LastFM API key (Optional) - - Change the port of the webbybits server ("Default ) + - Change the port of the webbybits server (Default: `19054` ) 3. Run `databaseGenerator.py` (Will try to use LastFM API key) * *The `databaseGenerator.py` will index all mp3 files, and save the information to `songDatabase.db`* * *If getting images, this process may take a long time with a large amount of mp3 files* @@ -34,6 +34,7 @@ webbyBits.py * *The port can be customized by editing the `.env` file* * *You can add an admin password at runtime with* `-a AdminPass` *as an atribute* * ***NOTE: Do not reuse ANY password for this, it is hashed but 100% unsecure. The best option is just a random string you write down once*** + * If this attribute isn't included a random string will be generated as the admin password * This is intended for protecting certain features for small closed events, not for public security You can now connect with the client and use the app as normal. \ @@ -88,12 +89,6 @@ From left to right: - *Because the volume can be controlled in the client, for best usage set your device volume as high as possible and turn it down using this slider* - QR code to allow others to connect to and use the Remote -## External Credits - - 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) - -*See `LICENSE.md` for redistribution and editing details.* - ### A quick note on the password feature The exact process of the password's plaintext scope is as follows @@ -102,4 +97,10 @@ The exact process of the password's plaintext scope is as follows - On the client, you type in the password and press enter. A function reads the value of the password box, saves the hash of that password to a variable, and sends it with all your requests. The plaintext is still stored in the inputbox, but if you delete it and don't press enter on the box again, the hash will be stored without keeping the plaintext. (I may change this behaviour so this box auto-clears when enter is pressed, maybe) -None of this is "secure", but it's better than sending plaintext passwords, which is what I was doing before. Hypothetically somebody who intercepted your packet where you sent the password can't get back the original plaintext, just the hash. \ No newline at end of file +None of this is "secure", but it's better than sending plaintext passwords, which is what I was doing before. Hypothetically somebody who intercepted your packet where you sent the password can't get back the original plaintext, just the hash. + +## External Credits + - 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) + +*See `LICENSE.md` for redistribution and editing details.* diff --git a/requirements.txt b/requirements.txt index 73c4c0a..ca37329 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ certifi==2024.2.2 charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 +dotenv Flask==3.0.3 Flask-Cors==4.0.1 idna==3.7 -- 2.49.1 From 9bdac82f10ef6cb7ab0283b964c22b47654d8a5e Mon Sep 17 00:00:00 2001 From: Kristy Fournier Date: Sun, 25 Jan 2026 19:04:02 -0500 Subject: [PATCH 2/7] In the thick of changing the responses to requests --- Client/index.html | 3 ++- Client/scripts.js | 31 ++++++++++++++++++++----------- Client/styles.css | 7 +++++++ Server/webbyBits.py | 33 +++++++++++++++++---------------- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/Client/index.html b/Client/index.html index 7172540..428fb2d 100644 --- a/Client/index.html +++ b/Client/index.html @@ -5,6 +5,7 @@ + @@ -107,7 +108,7 @@ changes visibility with JS-->

Wipe the playlist, except the currently playing song*

-

PartyJukebox is under an AGPLV3 liscense. You can access the source code here.

+

PartyJukebox is under an AGPLV3 liscense. You can access the source code here.

diff --git a/Client/scripts.js b/Client/scripts.js index 826709f..1d34c71 100644 --- a/Client/scripts.js +++ b/Client/scripts.js @@ -2,7 +2,7 @@ let ip; let alertTime = 2; 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 params = new URLSearchParams(location.search); @@ -46,8 +46,9 @@ async function getFromServer(bodyInfo, source="",password=adminPass) { "Content-type": "application/json; charset=UTF-8" } }); + const data = await response.json(); - if (data == ERR_NO_ADMIN) { + if (response.status == ERR_NO_ADMIN) { // im suprised i didn't comment on this already but this is kinda lame desing // its not wrong but you know // it is easy which i like @@ -56,9 +57,11 @@ async function getFromServer(bodyInfo, source="",password=adminPass) { } return await data; } catch(e) { + console.log("error print here:"); + console.log(e); if (e == "TypeError: Failed to fetch"){ alertText("Error: Can't Connect to Server (is the ip set?)") - } else if(e == "") { + } else if(e === "") { } else { alertText("Error: " + e); @@ -155,6 +158,13 @@ async function searchSongs(searchTerm){ newItem.appendChild(image); newItem.appendChild(head3); newItem.appendChild(head4); + // I like this concept but i'm leaving it out for now + // if(currentSongInJSON.lossless === 1) { + // let losslesstag = document.createElement("p"); + // losslesstag.textContent = "Ⓛ"; + // losslesstag.classList.add("lossless-tag"); + // newItem.appendChild(losslesstag); + // } document.getElementById("songlist").appendChild(newItem); } @@ -301,7 +311,7 @@ async function generateVisualPlaylist(conditions="") { let timeLeft =document.createElement("h5"); timeLeft.style.fontWeight = 100; try { - if (i == 0) { + if (i == 0) { // Only the first song in the loop gets a time head5.innerHTML="Playing"; if ((conditions != "skip-button")) { let mins = Math.floor(playlist[i]["time"]/60); @@ -313,7 +323,7 @@ async function generateVisualPlaylist(conditions="") { } }catch(err){ // i dont know why there's a try catch here but i'm leaving it i dont want to break something - console.log(err) + console.error(err) } let textdiv = document.createElement("div") textdiv.className="text" @@ -332,11 +342,9 @@ async function submitSong(songid) { let returncode = await getFromServer({song: songid}, "songadd"); if(returncode == ERR_NO_ADMIN) { // right now the error is alerted in getFromServer, maybe will change that - } - else if(returncode["error"]=="song-in-queue") { + } else if(returncode["error"]=="song-in-queue") { alertText("That song's about to play! Hang on!") - } - else { + } else { alertText("Added to Queue"); } } @@ -346,11 +354,10 @@ function checkWhatSongWasClicked(e) { if ((itemId.length-itemId.lastIndexOf("image") == 5) && itemId.lastIndexOf("image")!=-1) { itemId = itemId.slice(0,-6) } + let filenameSep = itemId.split('.') //i feel like later kristy won't apreciate this //one of my files was "file.MP3" so it didn't work //windows be like - let filenameSep = itemId.split('.') - if (VALID_FILE_EXT.includes(filenameSep[filenameSep.length-1].toLowerCase())) { submitSong(itemId); } @@ -443,6 +450,8 @@ document.getElementById("volumerange").onchange = async function() { if (returnValue == ERR_NO_ADMIN) { // alertText("Error: Admin restricted action"); // there's an admin restrict alert built into getFromServer + // i wanna put the volume slider back to where it was but idk a good way to keep the previous volume + checkSettings(false); } else if (returnValue["volumePassed"] !=0) { // i forgot about this, i had to do this because it confused the crap out of me one time // vlc doesn't let you change the volume of nothing, which makes sense if you think about it diff --git a/Client/styles.css b/Client/styles.css index 024e988..21f7b7c 100644 --- a/Client/styles.css +++ b/Client/styles.css @@ -134,6 +134,7 @@ h4 { .songlist > .item > h3, .songlist > .item > h4{ margin-left: 2px; margin-right: 2px; + margin: 5px; word-wrap: break-word; } @@ -152,6 +153,12 @@ h4 { width: 20%; min-width: 50px; } + +.lossless-tag { + width:16px; + padding: 1px; + margin-left: auto; +} /* playlist mode stuff */ .playlist { diff --git a/Server/webbyBits.py b/Server/webbyBits.py index 130131b..056d23f 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -10,10 +10,9 @@ parser.add_argument('-a','--admin',help="Add an admin password to be used in the args = parser.parse_args() dotenv.load_dotenv() portTheUserPicked=os.getenv("SERVER_PORT") -# Just a note that the return code "401" as of now is used to mean "you don't have the password" -# This is not great design, and the whole "returning string codes" thing is something to add to the todo list -# I mean returning 200 when no return is necesary i think is fine but we'll see -ERR_NO_ADMIN = "401" + +ERR_NO_ADMIN = ({"error":"no-admin"},401) +ERR_200 = ({"error":"OK"},200) if args.admin: ADMIN_PASS = hashlib.sha256(bytes(args.admin,'utf-8')).hexdigest() else: @@ -99,7 +98,8 @@ def playQueuedSongs(): # the above 2 means this only applies if (a song is playing or paused) and (the queue is empty) playlist.append(result[0][0]) # 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 + # this also has another useful affect that skips get "queued" to only 1 per second, that way somebody usually can't skip twice accidentally time.sleep(1) @app.route("/controls", methods=['POST']) @@ -112,26 +112,27 @@ def playerControls(): if recieveData["control"] == "play-pause": if ADMIN_PASS == recieveData['password'] or controlPerms["PP"]: player.pause() - return "200" + return ERR_200 else: return ERR_NO_ADMIN elif recieveData["control"] == "skip": if ADMIN_PASS == recieveData['password'] or controlPerms["SK"]: skipNow = True - return "200" + return ERR_200 else: return ERR_NO_ADMIN + # Maybe i should have put this next one in the "settings" section elif recieveData["control"] == "clear": if ADMIN_PASS == recieveData['password']: # this is only ever allowed with the adminpassword with playlistLock: playlist.clear() - return "200" + return ERR_200 else: return ERR_NO_ADMIN else: - return "400" + return {"error":"Not a valid control"},400 else: - return "400" + return {"error":"No control sent"},400 @app.route("/settings", methods=['POST']) def settingsControl(): @@ -149,14 +150,13 @@ def settingsControl(): elif recieveData["setting"] == "partymode-toggle": if ADMIN_PASS == recieveData['password'] or controlPerms["PM"]: partyMode = not(partyMode) - return "200" + return ERR_200 else: return ERR_NO_ADMIN elif recieveData["setting"] == "perms": - if ADMIN_PASS == recieveData["password"] and ADMIN_PASS: - #if an adminpass doesn't exist these perms can never be changed + if ADMIN_PASS == recieveData["password"]: controlPerms = recieveData["admin"] - return "200" + return ERR_200 else: return ERR_NO_ADMIN elif recieveData["setting"] == "getsettings": @@ -184,7 +184,8 @@ def searchSongDB(): "title": i[1], "artist": i[2], "art": i[3], - "length": i[4] + "length": i[4], + "lossless":i[5] } fileofDB.close() return tempdata @@ -201,7 +202,7 @@ def songadd(): # probably with a checkbox like the other admin controls if True: queueSong(recieveData['song']) - return "200" + return ERR_200 else: # the password is incorrect (technically a password not existing falls into the above case because controlPerms is never changed) return ERR_NO_ADMIN -- 2.49.1 From 0b64a6f29721b4033ba5d3fb441a3b138c82eeb0 Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:09:26 -0500 Subject: [PATCH 3/7] Okay the backend should be done so im committing now im gonna fix the frontend next --- Client/scripts.js | 7 +++---- Server/webbyBits.py | 27 ++++++++++++++++----------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Client/scripts.js b/Client/scripts.js index 1d34c71..8f556be 100644 --- a/Client/scripts.js +++ b/Client/scripts.js @@ -59,10 +59,8 @@ async function getFromServer(bodyInfo, source="",password=adminPass) { } catch(e) { console.log("error print here:"); console.log(e); - if (e == "TypeError: Failed to fetch"){ + if (e.contains("TypeError: Failed to fetch")){ alertText("Error: Can't Connect to Server (is the ip set?)") - } else if(e === "") { - } else { alertText("Error: " + e); } @@ -133,7 +131,8 @@ function searchSongsEnter(e) { async function searchSongs(searchTerm){ document.getElementById("songlist").innerHTML = "" - searchResults = await getFromServer({search:searchTerm},"search").then() + let fetchResults = await getFromServer({search:searchTerm},"search").then(); + let searchResults = fetchResults.data; //generate the visual song list for(var fileName in searchResults) { let currentSongInJSON = searchResults[fileName] diff --git a/Server/webbyBits.py b/Server/webbyBits.py index 056d23f..65cd09a 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -11,8 +11,8 @@ args = parser.parse_args() dotenv.load_dotenv() portTheUserPicked=os.getenv("SERVER_PORT") -ERR_NO_ADMIN = ({"error":"no-admin"},401) -ERR_200 = ({"error":"OK"},200) +ERR_NO_ADMIN = ({"error":"no-admin","data":None},401) +ERR_200 = ({"error":"OK","data":None},200) if args.admin: ADMIN_PASS = hashlib.sha256(bytes(args.admin,'utf-8')).hexdigest() else: @@ -130,9 +130,9 @@ def playerControls(): else: return ERR_NO_ADMIN else: - return {"error":"Not a valid control"},400 + return {"error":"Not a valid control","data":None},400 else: - return {"error":"No control sent"},400 + return {"error":"No control sent","data":None},400 @app.route("/settings", methods=['POST']) def settingsControl(): @@ -143,8 +143,11 @@ def settingsControl(): recieveData = request.get_json(force=True) if recieveData["setting"] == "volume": if ADMIN_PASS == recieveData['password'] or controlPerms["VOL"]: - volumePassed = player.audio_set_volume(int(recieveData["level"])) - return {"volumePassed":volumePassed} + if(recieveData["level"] <= 100 and recieveData["level"] >= 0): + volumePassed = player.audio_set_volume(int(recieveData["level"])) + return {"error":"ok","data":{"volumePassed":volumePassed}},200 + else: + return {"error":"Invalid volume level","data":None} else: return ERR_NO_ADMIN elif recieveData["setting"] == "partymode-toggle": @@ -161,9 +164,9 @@ def settingsControl(): return ERR_NO_ADMIN elif recieveData["setting"] == "getsettings": # probably should have made this a different request type or something but it works - return {"partymode":partyMode,"volume":player.audio_get_volume(),"admin":controlPerms} + return {"error":"ok","data":{"partymode":partyMode,"volume":player.audio_get_volume(),"admin":controlPerms}},200 else: - return "400" + return {"error":"Not a valid setting","data":None},400 @app.route("/search", methods=['POST']) def searchSongDB(): @@ -188,12 +191,13 @@ def searchSongDB(): "lossless":i[5] } fileofDB.close() - return tempdata + + return {"error":"ok","data":tempdata},200 @app.route("/songadd", methods=["POST"]) def songadd(): recieveData=request.get_json(force=True) - if (ADMIN_PASS and ADMIN_PASS == recieveData['password']) or controlPerms["AS"]: + if (ADMIN_PASS == recieveData['password']) or controlPerms["AS"]: # Password exists and is correct, or it's not restricted # if (recieveData['song'] in playlist): # return {"error":"song-in-queue"} @@ -239,7 +243,8 @@ def getPlaylist(): } tempPlaylist.append({i:k}) fileofDB.close() - return tempPlaylist + + return {"error":"ok","data":tempPlaylist} 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 -- 2.49.1 From 00550cca852fe7433a8257aa9de7686238ff8ab1 Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:39:17 -0500 Subject: [PATCH 4/7] I got distracted and broke something but the responses should be good now --- Client/scripts.js | 25 +++++++++++++++++-------- Server/webbyBits.py | 9 +++++---- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Client/scripts.js b/Client/scripts.js index 8f556be..9ecb8a8 100644 --- a/Client/scripts.js +++ b/Client/scripts.js @@ -2,7 +2,7 @@ let ip; let alertTime = 2; let adminPass = ""; -const ERR_NO_ADMIN = 401; // gonna use this later to refactor +const ERR_NO_ADMIN = 401; const VALID_FILE_EXT = ["mp3","flac","wav"]; const params = new URLSearchParams(location.search); @@ -54,12 +54,15 @@ async function getFromServer(bodyInfo, source="",password=adminPass) { // it is easy which i like // and it overrides any other non-async alerts which is nice alertText("Error: Admin restricted action") + } else if(response.status !== 200){ + alertText("Error: "+data.error); } + data["status"] = response.status; return await data; } catch(e) { console.log("error print here:"); console.log(e); - if (e.contains("TypeError: Failed to fetch")){ + if (e == "TypeError: Failed to fetch"){ alertText("Error: Can't Connect to Server (is the ip set?)") } else { alertText("Error: " + e); @@ -87,6 +90,8 @@ function getCookie(cname) { return ""; } //someone more organised than me would have set all these html elements to variables so they dont have to get them 50 times +// also someone who likes things not being dumb more than me would have separated the client and server buttons +let timer = null; async function controlButton(buttonType) { if (buttonType == "pp") { // Play-Pause button getFromServer({control: "play-pause"}, "controls") @@ -101,6 +106,9 @@ async function controlButton(buttonType) { document.getElementById("playlist-mode").style.display = "block"; document.getElementById("songlist-mode").style.display = "none"; document.getElementById("settings-mode").style.display = "none"; + timer = setInterval(() => { + + }) generateVisualPlaylist(); } else if (buttonType == "se") { //SearchMode button document.getElementById("songlist").innerHTML = "

Search to find songs!

"; @@ -246,7 +254,8 @@ async function checkSettings(skipServer=false) { } } //ping the server here - x = await getFromServer({setting: "getsettings"}, "settings"); + data = await getFromServer({setting: "getsettings"}, "settings"); + x = data["data"]; if (!(skipServer) || partyButtonState=="N/A") { if (x["partymode"] == false) { document.getElementById("partymode-button").innerHTML = "Off"; @@ -271,7 +280,8 @@ async function checkSettings(skipServer=false) { async function generateVisualPlaylist(conditions="") { document.getElementById("playlist").innerHTML = "

"; - playlist = await getFromServer(null, "playlist"); + data = await getFromServer(null, "playlist"); + playlist = data["data"]; playlist = Object.values(playlist).map(obj => { const filename = Object.keys(obj)[0]; // Get the filename const songData = obj[filename]; // Get the song metadata @@ -446,18 +456,17 @@ document.getElementById("volumerange").onchange = async function() { // there is no reason for this not to be a defined function // FIX THIS let returnValue = await getFromServer({setting:"volume",level:this.value}, "settings") - if (returnValue == ERR_NO_ADMIN) { + if (returnValue["status"] == ERR_NO_ADMIN) { // alertText("Error: Admin restricted action"); // there's an admin restrict alert built into getFromServer // i wanna put the volume slider back to where it was but idk a good way to keep the previous volume checkSettings(false); - } else if (returnValue["volumePassed"] !=0) { + } else if (returnValue["data"]["volumePassed"] !=0) { // i forgot about this, i had to do this because it confused the crap out of me one time // vlc doesn't let you change the volume of nothing, which makes sense if you think about it alertText("Nothing is playing") document.getElementById("volumerange").value = -1 - } - else if (this.value == 0) { + } else if (this.value == 0) { alertText("The volume is now set to 0 (Pause?)") } else { alertText("The volume is now set to " + this.value.toString()) diff --git a/Server/webbyBits.py b/Server/webbyBits.py index 65cd09a..a506e82 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -143,11 +143,12 @@ def settingsControl(): recieveData = request.get_json(force=True) if recieveData["setting"] == "volume": if ADMIN_PASS == recieveData['password'] or controlPerms["VOL"]: - if(recieveData["level"] <= 100 and recieveData["level"] >= 0): - volumePassed = player.audio_set_volume(int(recieveData["level"])) + volumeLevel = int(recieveData["level"]) + if(volumeLevel <= 100 and volumeLevel >= 0): + volumePassed = player.audio_set_volume(volumeLevel) return {"error":"ok","data":{"volumePassed":volumePassed}},200 else: - return {"error":"Invalid volume level","data":None} + return {"error":"Invalid volume level","data":None},422 else: return ERR_NO_ADMIN elif recieveData["setting"] == "partymode-toggle": @@ -166,7 +167,7 @@ def settingsControl(): # probably should have made this a different request type or something but it works return {"error":"ok","data":{"partymode":partyMode,"volume":player.audio_get_volume(),"admin":controlPerms}},200 else: - return {"error":"Not a valid setting","data":None},400 + return {"error":"Not a valid setting","data":None},422 @app.route("/search", methods=['POST']) def searchSongDB(): -- 2.49.1 From 417ecc8cede000e8b614ae940875055afcf38403 Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:50:59 -0500 Subject: [PATCH 5/7] messing around, will fix later --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3920035..87c9ad3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ server/sound/ *.db start.bat -.env \ No newline at end of file +.env +venv/ \ No newline at end of file -- 2.49.1 From 2002dd1afaa4bd7ff70bada3567bfdd253f21395 Mon Sep 17 00:00:00 2001 From: Kristy Fournier Date: Mon, 26 Jan 2026 12:15:56 -0500 Subject: [PATCH 6/7] removed broken timer, finalised new responses --- Client/scripts.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Client/scripts.js b/Client/scripts.js index 9ecb8a8..8b97fbd 100644 --- a/Client/scripts.js +++ b/Client/scripts.js @@ -47,21 +47,24 @@ async function getFromServer(bodyInfo, source="",password=adminPass) { } }); - const data = await response.json(); + let data = await response.json(); // original json if (response.status == ERR_NO_ADMIN) { // im suprised i didn't comment on this already but this is kinda lame desing // its not wrong but you know // it is easy which i like // and it overrides any other non-async alerts which is nice alertText("Error: Admin restricted action") - } else if(response.status !== 200){ + } else if(!response.ok){ alertText("Error: "+data.error); } + // we add some information from the response just in case it is needed + data["ok"] = response.ok; data["status"] = response.status; + // console.log(data); return await data; } catch(e) { - console.log("error print here:"); - console.log(e); + // console.log("error print here:"); + // console.log(e); if (e == "TypeError: Failed to fetch"){ alertText("Error: Can't Connect to Server (is the ip set?)") } else { @@ -91,7 +94,6 @@ function getCookie(cname) { } //someone more organised than me would have set all these html elements to variables so they dont have to get them 50 times // also someone who likes things not being dumb more than me would have separated the client and server buttons -let timer = null; async function controlButton(buttonType) { if (buttonType == "pp") { // Play-Pause button getFromServer({control: "play-pause"}, "controls") @@ -106,9 +108,6 @@ async function controlButton(buttonType) { document.getElementById("playlist-mode").style.display = "block"; document.getElementById("songlist-mode").style.display = "none"; document.getElementById("settings-mode").style.display = "none"; - timer = setInterval(() => { - - }) generateVisualPlaylist(); } else if (buttonType == "se") { //SearchMode button document.getElementById("songlist").innerHTML = "

Search to find songs!

"; -- 2.49.1 From fce09edfc555a54a97cf6a6ca65d4df45c289c39 Mon Sep 17 00:00:00 2001 From: Kristy Fournier Date: Mon, 26 Jan 2026 12:30:08 -0500 Subject: [PATCH 7/7] Added proper handling of internal errors (should be no 500s anywhere) --- Client/scripts.js | 1 - Server/webbyBits.py | 135 ++++++++++++++++++++++++-------------------- 2 files changed, 75 insertions(+), 61 deletions(-) diff --git a/Client/scripts.js b/Client/scripts.js index 8b97fbd..cb7361e 100644 --- a/Client/scripts.js +++ b/Client/scripts.js @@ -38,7 +38,6 @@ async function getFromServer(bodyInfo, source="",password=adminPass) { // the currently set password is always included in every request bodyInfo["password"] = password; } - // console.log(bodyInfo); const response = await fetch("http://"+ip+"/"+source, { method: "POST", body: JSON.stringify(bodyInfo), diff --git a/Server/webbyBits.py b/Server/webbyBits.py index a506e82..d0d2e36 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -13,6 +13,7 @@ portTheUserPicked=os.getenv("SERVER_PORT") ERR_NO_ADMIN = ({"error":"no-admin","data":None},401) ERR_200 = ({"error":"OK","data":None},200) +ERR_MISSING_ARGS = ({"error":"Request missing required arguments","data":None}),400 if args.admin: ADMIN_PASS = hashlib.sha256(bytes(args.admin,'utf-8')).hexdigest() else: @@ -108,7 +109,7 @@ def playerControls(): global skipNow global partyMode recieveData=request.get_json(force=True) - if recieveData["control"] != None: + try: if recieveData["control"] == "play-pause": if ADMIN_PASS == recieveData['password'] or controlPerms["PP"]: player.pause() @@ -131,8 +132,8 @@ def playerControls(): return ERR_NO_ADMIN else: return {"error":"Not a valid control","data":None},400 - else: - return {"error":"No control sent","data":None},400 + except KeyError: + return ERR_MISSING_ARGS @app.route("/settings", methods=['POST']) def settingsControl(): @@ -141,76 +142,90 @@ def settingsControl(): global partyMode global player recieveData = request.get_json(force=True) - if recieveData["setting"] == "volume": - if ADMIN_PASS == recieveData['password'] or controlPerms["VOL"]: - volumeLevel = int(recieveData["level"]) - if(volumeLevel <= 100 and volumeLevel >= 0): - volumePassed = player.audio_set_volume(volumeLevel) - return {"error":"ok","data":{"volumePassed":volumePassed}},200 + try: + if recieveData["setting"] == "volume": + if ADMIN_PASS == recieveData['password'] or controlPerms["VOL"]: + volumeLevel = int(recieveData["level"]) + if(volumeLevel <= 100 and volumeLevel >= 0): + volumePassed = player.audio_set_volume(volumeLevel) + return {"error":"ok","data":{"volumePassed":volumePassed}},200 + else: + return {"error":"Invalid volume level","data":None},422 else: - return {"error":"Invalid volume level","data":None},422 + return ERR_NO_ADMIN + elif recieveData["setting"] == "partymode-toggle": + if ADMIN_PASS == recieveData['password'] or controlPerms["PM"]: + partyMode = not(partyMode) + return ERR_200 + else: + return ERR_NO_ADMIN + elif recieveData["setting"] == "perms": + if ADMIN_PASS == recieveData["password"]: + controlPerms = recieveData["admin"] + return ERR_200 + else: + return ERR_NO_ADMIN + elif recieveData["setting"] == "getsettings": + # probably should have made this a different request type or something but it works + return {"error":"ok","data":{"partymode":partyMode,"volume":player.audio_get_volume(),"admin":controlPerms}},200 else: - return ERR_NO_ADMIN - elif recieveData["setting"] == "partymode-toggle": - if ADMIN_PASS == recieveData['password'] or controlPerms["PM"]: - partyMode = not(partyMode) - return ERR_200 - else: - return ERR_NO_ADMIN - elif recieveData["setting"] == "perms": - if ADMIN_PASS == recieveData["password"]: - controlPerms = recieveData["admin"] - return ERR_200 - else: - return ERR_NO_ADMIN - elif recieveData["setting"] == "getsettings": - # probably should have made this a different request type or something but it works - return {"error":"ok","data":{"partymode":partyMode,"volume":player.audio_get_volume(),"admin":controlPerms}},200 - else: - return {"error":"Not a valid setting","data":None},422 + return {"error":"Not a valid setting","data":None},400 + except: + return ERR_MISSING_ARGS @app.route("/search", methods=['POST']) def searchSongDB(): recieveData=request.get_json(force=True) fileofDB = sql.connect("songDatabase.db") songDatabase = fileofDB.cursor() - results = [] - if (recieveData['search'] == ""): - songDatabase.execute("SELECT * FROM virtualSongs") - results = songDatabase.fetchall() - else: - songDatabase.execute("SELECT * FROM virtualSongs WHERE virtualSongs MATCH ?",[recieveData['search']]) - results = songDatabase.fetchall() - tempdata = {} - # this is a temporary solution so i dont have to change the client - for i in results: - tempdata[i[0]] = { - "title": i[1], - "artist": i[2], - "art": i[3], - "length": i[4], - "lossless":i[5] - } - fileofDB.close() + try: + results = [] + if (recieveData['search'] == ""): + songDatabase.execute("SELECT * FROM virtualSongs") + results = songDatabase.fetchall() + else: + songDatabase.execute("SELECT * FROM virtualSongs WHERE virtualSongs MATCH ?",[recieveData['search']]) + results = songDatabase.fetchall() + tempdata = {} + # this is a temporary solution so i dont have to change the client + for i in results: + tempdata[i[0]] = { + "title": i[1], + "artist": i[2], + "art": i[3], + "length": i[4], + "lossless":i[5] + } + fileofDB.close() + + return {"error":"ok","data":tempdata},200 + except KeyError: + fileofDB.close() + return ERR_MISSING_ARGS + except sql.OperationalError: + fileofDB.close() + return ({"error":"Invalid search, sorry!","data":None},422) - return {"error":"ok","data":tempdata},200 @app.route("/songadd", methods=["POST"]) def songadd(): recieveData=request.get_json(force=True) - if (ADMIN_PASS == recieveData['password']) or controlPerms["AS"]: - # Password exists and is correct, or it's not restricted - # if (recieveData['song'] in playlist): - # return {"error":"song-in-queue"} - # else: - # Right now the above is disabled since i want to make it optional first - # probably with a checkbox like the other admin controls - if True: - queueSong(recieveData['song']) - return ERR_200 - else: - # the password is incorrect (technically a password not existing falls into the above case because controlPerms is never changed) - return ERR_NO_ADMIN + try: + if (ADMIN_PASS == recieveData['password']) or controlPerms["AS"]: + # Password exists and is correct, or it's not restricted + # if (recieveData['song'] in playlist): + # return {"error":"song-in-queue"} + # else: + # Right now the above is disabled since i want to make it optional first + # probably with a checkbox like the other admin controls + if True: + queueSong(recieveData['song']) + return ERR_200 + else: + # the password is incorrect (technically a password not existing falls into the above case because controlPerms is never changed) + return ERR_NO_ADMIN + except KeyError: + return ERR_MISSING_ARGS @app.route("/playlist", methods=["POST"]) def getPlaylist(): -- 2.49.1