From 4d3c707301a7037daf7ddeac6f05b39b3080b4e8 Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:58:16 -0500 Subject: [PATCH 01/19] Moved Static Client --- {Client => Server/Client}/ext/popper.js | 0 {Client => Server/Client}/ext/qrcode.min.js | 0 {Client => Server/Client}/favicon.ico | Bin {Client => Server/Client}/images/Icon-144.png | Bin .../Client}/images/Screenshot-Main-Desktop.png | Bin .../Client}/images/Screenshot-Main-Mobile.png | Bin {Client => Server/Client}/images/placeholder.png | Bin {Client => Server/Client}/images/play-pause-old.png | Bin {Client => Server/Client}/images/play-pause.png | Bin {Client => Server/Client}/images/playlist.png | Bin {Client => Server/Client}/images/search.png | Bin {Client => Server/Client}/images/settings.png | Bin {Client => Server/Client}/images/skip.png | Bin {Client => Server/Client}/index.html | 0 {Client => Server/Client}/manifest.json | 0 {Client => Server/Client}/scripts.js | 0 {Client => Server/Client}/styles.css | 0 17 files changed, 0 insertions(+), 0 deletions(-) rename {Client => Server/Client}/ext/popper.js (100%) rename {Client => Server/Client}/ext/qrcode.min.js (100%) rename {Client => Server/Client}/favicon.ico (100%) rename {Client => Server/Client}/images/Icon-144.png (100%) rename {Client => Server/Client}/images/Screenshot-Main-Desktop.png (100%) rename {Client => Server/Client}/images/Screenshot-Main-Mobile.png (100%) rename {Client => Server/Client}/images/placeholder.png (100%) rename {Client => Server/Client}/images/play-pause-old.png (100%) rename {Client => Server/Client}/images/play-pause.png (100%) rename {Client => Server/Client}/images/playlist.png (100%) rename {Client => Server/Client}/images/search.png (100%) rename {Client => Server/Client}/images/settings.png (100%) rename {Client => Server/Client}/images/skip.png (100%) rename {Client => Server/Client}/index.html (100%) rename {Client => Server/Client}/manifest.json (100%) rename {Client => Server/Client}/scripts.js (100%) rename {Client => Server/Client}/styles.css (100%) diff --git a/Client/ext/popper.js b/Server/Client/ext/popper.js similarity index 100% rename from Client/ext/popper.js rename to Server/Client/ext/popper.js diff --git a/Client/ext/qrcode.min.js b/Server/Client/ext/qrcode.min.js similarity index 100% rename from Client/ext/qrcode.min.js rename to Server/Client/ext/qrcode.min.js diff --git a/Client/favicon.ico b/Server/Client/favicon.ico similarity index 100% rename from Client/favicon.ico rename to Server/Client/favicon.ico diff --git a/Client/images/Icon-144.png b/Server/Client/images/Icon-144.png similarity index 100% rename from Client/images/Icon-144.png rename to Server/Client/images/Icon-144.png diff --git a/Client/images/Screenshot-Main-Desktop.png b/Server/Client/images/Screenshot-Main-Desktop.png similarity index 100% rename from Client/images/Screenshot-Main-Desktop.png rename to Server/Client/images/Screenshot-Main-Desktop.png diff --git a/Client/images/Screenshot-Main-Mobile.png b/Server/Client/images/Screenshot-Main-Mobile.png similarity index 100% rename from Client/images/Screenshot-Main-Mobile.png rename to Server/Client/images/Screenshot-Main-Mobile.png diff --git a/Client/images/placeholder.png b/Server/Client/images/placeholder.png similarity index 100% rename from Client/images/placeholder.png rename to Server/Client/images/placeholder.png diff --git a/Client/images/play-pause-old.png b/Server/Client/images/play-pause-old.png similarity index 100% rename from Client/images/play-pause-old.png rename to Server/Client/images/play-pause-old.png diff --git a/Client/images/play-pause.png b/Server/Client/images/play-pause.png similarity index 100% rename from Client/images/play-pause.png rename to Server/Client/images/play-pause.png diff --git a/Client/images/playlist.png b/Server/Client/images/playlist.png similarity index 100% rename from Client/images/playlist.png rename to Server/Client/images/playlist.png diff --git a/Client/images/search.png b/Server/Client/images/search.png similarity index 100% rename from Client/images/search.png rename to Server/Client/images/search.png diff --git a/Client/images/settings.png b/Server/Client/images/settings.png similarity index 100% rename from Client/images/settings.png rename to Server/Client/images/settings.png diff --git a/Client/images/skip.png b/Server/Client/images/skip.png similarity index 100% rename from Client/images/skip.png rename to Server/Client/images/skip.png diff --git a/Client/index.html b/Server/Client/index.html similarity index 100% rename from Client/index.html rename to Server/Client/index.html diff --git a/Client/manifest.json b/Server/Client/manifest.json similarity index 100% rename from Client/manifest.json rename to Server/Client/manifest.json diff --git a/Client/scripts.js b/Server/Client/scripts.js similarity index 100% rename from Client/scripts.js rename to Server/Client/scripts.js diff --git a/Client/styles.css b/Server/Client/styles.css similarity index 100% rename from Client/styles.css rename to Server/Client/styles.css From 66b4ce33bff3f5647e4f8096e5649985c9db3b0f Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:03:15 -0500 Subject: [PATCH 02/19] so many moves --- Server/{Client => static}/ext/popper.js | 0 Server/{Client => static}/ext/qrcode.min.js | 0 Server/{Client => static}/favicon.ico | Bin Server/{Client => static}/images/Icon-144.png | Bin .../images/Screenshot-Main-Desktop.png | Bin .../images/Screenshot-Main-Mobile.png | Bin Server/{Client => static}/images/placeholder.png | Bin Server/{Client => static}/images/play-pause-old.png | Bin Server/{Client => static}/images/play-pause.png | Bin Server/{Client => static}/images/playlist.png | Bin Server/{Client => static}/images/search.png | Bin Server/{Client => static}/images/settings.png | Bin Server/{Client => static}/images/skip.png | Bin Server/{Client => static}/manifest.json | 0 Server/{Client => static}/scripts.js | 0 Server/{Client => static}/styles.css | 0 Server/{Client => templates}/index.html | 0 Server/webbyBits.py | 6 +++++- 18 files changed, 5 insertions(+), 1 deletion(-) rename Server/{Client => static}/ext/popper.js (100%) rename Server/{Client => static}/ext/qrcode.min.js (100%) rename Server/{Client => static}/favicon.ico (100%) rename Server/{Client => static}/images/Icon-144.png (100%) rename Server/{Client => static}/images/Screenshot-Main-Desktop.png (100%) rename Server/{Client => static}/images/Screenshot-Main-Mobile.png (100%) rename Server/{Client => static}/images/placeholder.png (100%) rename Server/{Client => static}/images/play-pause-old.png (100%) rename Server/{Client => static}/images/play-pause.png (100%) rename Server/{Client => static}/images/playlist.png (100%) rename Server/{Client => static}/images/search.png (100%) rename Server/{Client => static}/images/settings.png (100%) rename Server/{Client => static}/images/skip.png (100%) rename Server/{Client => static}/manifest.json (100%) rename Server/{Client => static}/scripts.js (100%) rename Server/{Client => static}/styles.css (100%) rename Server/{Client => templates}/index.html (100%) diff --git a/Server/Client/ext/popper.js b/Server/static/ext/popper.js similarity index 100% rename from Server/Client/ext/popper.js rename to Server/static/ext/popper.js diff --git a/Server/Client/ext/qrcode.min.js b/Server/static/ext/qrcode.min.js similarity index 100% rename from Server/Client/ext/qrcode.min.js rename to Server/static/ext/qrcode.min.js diff --git a/Server/Client/favicon.ico b/Server/static/favicon.ico similarity index 100% rename from Server/Client/favicon.ico rename to Server/static/favicon.ico diff --git a/Server/Client/images/Icon-144.png b/Server/static/images/Icon-144.png similarity index 100% rename from Server/Client/images/Icon-144.png rename to Server/static/images/Icon-144.png diff --git a/Server/Client/images/Screenshot-Main-Desktop.png b/Server/static/images/Screenshot-Main-Desktop.png similarity index 100% rename from Server/Client/images/Screenshot-Main-Desktop.png rename to Server/static/images/Screenshot-Main-Desktop.png diff --git a/Server/Client/images/Screenshot-Main-Mobile.png b/Server/static/images/Screenshot-Main-Mobile.png similarity index 100% rename from Server/Client/images/Screenshot-Main-Mobile.png rename to Server/static/images/Screenshot-Main-Mobile.png diff --git a/Server/Client/images/placeholder.png b/Server/static/images/placeholder.png similarity index 100% rename from Server/Client/images/placeholder.png rename to Server/static/images/placeholder.png diff --git a/Server/Client/images/play-pause-old.png b/Server/static/images/play-pause-old.png similarity index 100% rename from Server/Client/images/play-pause-old.png rename to Server/static/images/play-pause-old.png diff --git a/Server/Client/images/play-pause.png b/Server/static/images/play-pause.png similarity index 100% rename from Server/Client/images/play-pause.png rename to Server/static/images/play-pause.png diff --git a/Server/Client/images/playlist.png b/Server/static/images/playlist.png similarity index 100% rename from Server/Client/images/playlist.png rename to Server/static/images/playlist.png diff --git a/Server/Client/images/search.png b/Server/static/images/search.png similarity index 100% rename from Server/Client/images/search.png rename to Server/static/images/search.png diff --git a/Server/Client/images/settings.png b/Server/static/images/settings.png similarity index 100% rename from Server/Client/images/settings.png rename to Server/static/images/settings.png diff --git a/Server/Client/images/skip.png b/Server/static/images/skip.png similarity index 100% rename from Server/Client/images/skip.png rename to Server/static/images/skip.png diff --git a/Server/Client/manifest.json b/Server/static/manifest.json similarity index 100% rename from Server/Client/manifest.json rename to Server/static/manifest.json diff --git a/Server/Client/scripts.js b/Server/static/scripts.js similarity index 100% rename from Server/Client/scripts.js rename to Server/static/scripts.js diff --git a/Server/Client/styles.css b/Server/static/styles.css similarity index 100% rename from Server/Client/styles.css rename to Server/static/styles.css diff --git a/Server/Client/index.html b/Server/templates/index.html similarity index 100% rename from Server/Client/index.html rename to Server/templates/index.html diff --git a/Server/webbyBits.py b/Server/webbyBits.py index 25e245b..fea2afe 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -1,5 +1,5 @@ from flask import Flask -from flask import request +from flask import request,render_template from flask_cors import CORS from flask_socketio import SocketIO import sqlite3 as sql @@ -143,6 +143,10 @@ def playQueuedSongs(): def handleConnect(): pass +@app.route("/",methods=['GET']) +def returnStaticFile(): + return render_template("index.html") + @app.route("/controls", methods=['POST']) def playerControls(): # recieve control inputs (play/pause and skip) from the webUI From f6e0049229738f34311d0ca762ad86f927b7b3c1 Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:50:57 -0500 Subject: [PATCH 03/19] Adjusted for not needing to set an ip anymore --- Server/static/scripts.js | 24 ++++++++++++------------ Server/templates/index.html | 24 ++++++++++++------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Server/static/scripts.js b/Server/static/scripts.js index cd7cb03..c089880 100644 --- a/Server/static/scripts.js +++ b/Server/static/scripts.js @@ -1,5 +1,5 @@ // set all the global stuff -let ip; +let ip = URL.parse(document.URL).host; let alertTime = 2; let adminPass = ""; let justSkipped = false; @@ -187,7 +187,7 @@ async function searchSongs(searchTerm){ } image.src = currentSongInJSON["art"]; } catch(err){ - image.src = "./images/placeholder.png"; + image.src = "/static/images/placeholder.png"; } image.id = String(fileName)+" image"; let head3 = document.createElement("h3"); @@ -317,12 +317,12 @@ async function updateSingleSetting(data) { async function checkSettings(skipServer=false) { //check client stuff first so if the server doesn't exist it can still be changed and seen - if (ip.slice(-5)=="19054") { - // don't show the port if it is the default - document.getElementById("iptextbox").value = ip.slice(0,-6) - } else { - document.getElementById("iptextbox").value = ip; - } + // if (ip.slice(-5)=="19054") { + // // don't show the port if it is the default + // document.getElementById("iptextbox").value = ip.slice(0,-6) + // } else { + // document.getElementById("iptextbox").value = ip; + // } qrCodeGenerate() document.getElementById("alerttimetextbox").value = alertTime partyButtonState = document.getElementById("partymode-button").innerHTML; @@ -372,7 +372,7 @@ async function addToPlaylist(songObject) { } image.src = songObject[newItem.id]["art"]; } catch(err){ - image.src = "./images/placeholder.png"; + image.src = "static/images/placeholder.png"; } image.id = String(songObject[newItem.id])+" image"; let head3 = document.createElement("h3"); @@ -462,7 +462,7 @@ async function generateVisualPlaylist(conditions="") { } image.src = playlist[i]["art"]; } catch(err){ - image.src = "./images/placeholder.png"; + image.src = "/static/images/placeholder.png"; } image.id = String(fileName)+" image"; let head3 = document.createElement("h3"); @@ -648,7 +648,7 @@ document.getElementById("search-button").addEventListener('click', function(){co document.getElementById("skip-button").addEventListener('click',function(){controlButton("sk")}); document.getElementById("go-search").addEventListener('click', function(){searchSongs(document.getElementById("songsearch").value)}) document.getElementById("songsearch").addEventListener('keydown', function(e){searchSongsEnter(e)}); -document.getElementById("iptextbox").addEventListener('keydown', function(e){ipSetEnter(e)}); +// document.getElementById("iptextbox").addEventListener('keydown', function(e){ipSetEnter(e)}); document.getElementById("alerttimetextbox").addEventListener('keydown', function(e){alertTimeEnter(e)}); document.getElementById("adminpasswordbox").addEventListener('keydown',function(e){adminPassEnter(e)}); document.getElementById("admincheckholder").addEventListener('click',function(e){submitPerms(e)}); @@ -675,7 +675,7 @@ if (ip == null || ip=="") { ip=getCookie("ip") } if (ip==null || ip==""){ - ip = "" + ip = URL.parse(document.URL).host; } // saving the cookies (don't tell the EU) diff --git a/Server/templates/index.html b/Server/templates/index.html index 70b36c0..3ec8ddc 100644 --- a/Server/templates/index.html +++ b/Server/templates/index.html @@ -3,8 +3,8 @@ Jukebox Controller - - + + @@ -12,7 +12,7 @@ - +

Jukebox Remote

@@ -63,11 +63,11 @@ changes visibility with JS-->

Opposite of light mode

-
+

Alert Time:

How long alerts stay on screen for (seconds)

@@ -119,15 +119,15 @@ changes visibility with JS-->
- Playlist - Play pause - Skip - Search + Playlist + Play pause + Skip + Search
- settings + settings
- - + + \ No newline at end of file From e14484835e7045a92b010cec6366b3d776f9ba7c Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:55:47 -0500 Subject: [PATCH 04/19] Get rid of cors, add static favicon --- Server/templates/index.html | 3 ++- Server/webbyBits.py | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Server/templates/index.html b/Server/templates/index.html index 0811395..e8d0eb9 100644 --- a/Server/templates/index.html +++ b/Server/templates/index.html @@ -4,7 +4,8 @@ Jukebox Controller - + + diff --git a/Server/webbyBits.py b/Server/webbyBits.py index 0017481..d519822 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -66,10 +66,8 @@ player = vlcInstance.media_player_new() # for client side volume to work as well as possible, set system volume to 100 and control in app player.audio_set_volume(100) app = Flask(__name__) -# because you are POSTing from another domain to this one, you need CORS -CORS(app) # Replace the star with the frontend domain if you dislike being hacked -socketio = SocketIO(app, cors_allowed_origins="*") +socketio = SocketIO(app) def queueSong(song): with playlistLock: From 71228d8cccadb39ba1489cd24fd119317b280811 Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:43:04 -0500 Subject: [PATCH 05/19] Update webbyBits.py --- Server/webbyBits.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Server/webbyBits.py b/Server/webbyBits.py index d519822..ff78ec2 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -66,7 +66,6 @@ player = vlcInstance.media_player_new() # for client side volume to work as well as possible, set system volume to 100 and control in app player.audio_set_volume(100) app = Flask(__name__) -# Replace the star with the frontend domain if you dislike being hacked socketio = SocketIO(app) def queueSong(song): From 419eed375ce46a9308168010a66eb59582d68ee4 Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:40:21 -0500 Subject: [PATCH 06/19] Changes to adjust for no longer having an ipbox --- Server/static/scripts.js | 48 +--------------------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/Server/static/scripts.js b/Server/static/scripts.js index 25ce887..b955dd6 100644 --- a/Server/static/scripts.js +++ b/Server/static/scripts.js @@ -81,7 +81,7 @@ async function getFromServer(bodyInfo, source="", secure=false, password=adminPa // console.log("error print here:"); // console.log(e); if (e.toString().includes("TypeError: Failed to fetch")){ - alertText("Error: Can't Connect to Server (is the ip set?)") + alertText("Error: Can't Connect to Server") } else { alertText(e); } @@ -226,38 +226,6 @@ function alertTimeSet(time) { alertText("Alerts stay on screen for " + alertTime.toString() + " seconds") } -function ipSetEnter(e){ - if (e.key == "Enter") { - e.preventDefault(); - // why on gosh's green earth am i sending a value here? - // im gonna get rid of all these individual "enter" dectectors and do something - // like i did for the keyboard selection of elements - // basically just if(e==click || e.key == enter) - ipSetter(document.getElementById("iptextbox").value) - } -} - -function ipSetter(){ - ipBox = document.getElementById("iptextbox").value - if (ipBox == "") { - alertText("Your IP is set to "+ip) - } else { - if (ipBox.includes(":")) { - port = ipBox.slice(ipBox.indexOf(":")+1) - ip = ipBox; - document.cookie = "ip="+ip+"; path=/;" - alertText("Your IP is now set to "+ip.slice(0, ipBox.indexOf(":"))+" at port "+port) - } else { - ip = ipBox + ":19054" - document.cookie = "ip="+ip+"; path=/;" - 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() - -} - function qrCodeGenerate() { let tempURL = "http://" + document.location.href.split("/")[2] + "/?ip=" + ip; document.getElementById("qrcode").innerHTML = ""; @@ -664,20 +632,6 @@ document.getElementById("songlist").addEventListener('click', function(e){checkW // let tempWidth = document.getElementById('controls').clientWidth; // document.getElementById("controls").style.marginLeft = "-"+String(parseInt(tempWidth/2))+"px"; -//for my use case (my immediate family), they dont know how to set an ip -//using this allows the creator of the link for, a qr code for example, to set the ip before distributing the code, and it would all work smoothly -//example (http://192.168.1.100:8000/?ip=192.168.1.100:19054 sets the ip to the same host at the default port) -//the port must be set manually using this method, but only has to be done once for the url that ends up being shared - -//tries the url first, then the cookie, then the default -ip = params.get("ip") -if (ip == null || ip=="") { - ip=getCookie("ip") -} -if (ip==null || ip==""){ - ip = URL.parse(document.URL).host; -} - // saving the cookies (don't tell the EU) document.cookie = "ip="+ip+"; path=/;" From f95ecc78d47d92ed0ed41155a81772b0a8aa428c Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:05:32 -0500 Subject: [PATCH 07/19] Multiple fixes - Fixed pwa location in manifest.json - made database generator check the .env for location rather than runtime attribute - fixed dark mode preference based on cookie/browser preference - changed to socketio.sleep for compatibility --- Server/databaseGenerator.py | 12 ++++++------ Server/static/manifest.json | 2 +- Server/static/scripts.js | 7 +++++-- Server/webbyBits.py | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Server/databaseGenerator.py b/Server/databaseGenerator.py index ebf8263..6a4f09a 100644 --- a/Server/databaseGenerator.py +++ b/Server/databaseGenerator.py @@ -11,18 +11,18 @@ parser=argparse.ArgumentParser(description="Options for the generation of the so # parser.add_argument('-k','--apikey', help='String: LastFM api key', default="") parser.add_argument('-m', '--mode', help='new/update: Remake database or update current', default= "update") parser.add_argument('-a', '--art', help="True/False: Add art to the database using LastFm (takes minimum 0.25s per song)", default="True") -parser.add_argument('-d','--directory',help="Directory of the song files", default="./sound/") +# parser.add_argument('-d','--directory',help="Directory of the song files", default="./sound/") args = parser.parse_args() dotenv.load_dotenv() apikeylastfm = os.getenv("API_KEY") soundLocation = os.getenv("DIRECTORY") # apikeylastfm = args.apikey -if args.directory[-1] == "/" or args.directory[-1] == "\\": - soundLocation = args.directory -elif "/" in args.directory: - soundLocation = args.directory + "/" +if soundLocation[-1] == "/" or soundLocation[-1] == "\\": + soundLocation = soundLocation +elif "/" in soundLocation: + soundLocation = soundLocation + "/" else: - soundLocation = args.directory + "\\" + soundLocation = soundLocation + "\\" songFiles = os.listdir(soundLocation) fileOfDB = sql.connect("songDatabase.db") diff --git a/Server/static/manifest.json b/Server/static/manifest.json index 975ff13..9ab490f 100644 --- a/Server/static/manifest.json +++ b/Server/static/manifest.json @@ -2,7 +2,7 @@ "name": "Jukebox Remote", "short_name": "Jukebox Remote", "description": "Controller for the PartyJukebox server app.", - "start_url": "index.html", + "start_url": "/", "display": "standalone", "background_color": "#eeeeee", "theme_color": "#eeeeee", diff --git a/Server/static/scripts.js b/Server/static/scripts.js index b955dd6..2c441b9 100644 --- a/Server/static/scripts.js +++ b/Server/static/scripts.js @@ -15,7 +15,7 @@ let currentlyPlaying = false; const params = new URLSearchParams(location.search); let darkmodetemp = getCookie("darkmode"); -if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { +if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && darkmodetemp === undefined) { darkmodetemp = "true"; } if(darkmodetemp === "") { @@ -191,8 +191,10 @@ async function searchSongs(searchTerm){ } image.id = String(fileName)+" image"; let head3 = document.createElement("h3"); + head3.id = fileName; head3.innerText = currentSongInJSON["title"]; let head4 = document.createElement("h4"); + head4.id = fileName; head4.innerText = currentSongInJSON["artist"]; newItem.appendChild(image); newItem.appendChild(head3); @@ -489,6 +491,7 @@ async function submitSong(songid) { function checkWhatSongWasClicked(e) { if(e.type == "click" || e.key == "Enter") { itemId = e.srcElement.id; + // console.log(e.srcElement); if ((itemId.length-itemId.lastIndexOf("image") == 5) && itemId.lastIndexOf("image")!=-1) { itemId = itemId.slice(0,-6) } @@ -565,7 +568,7 @@ async function submitPerms(e) { async function clearPlaylist() { let returncode = await getFromServer({control:"clear"},"controls"); - if(returncode == ERR_NO_ADMIN || returncode == null) { + if(returncode["status"] === ERR_NO_ADMIN || returncode == null) { // alertText("Admin Restricted ") // there's an admin restrict alert built into getFromServer } else { diff --git a/Server/webbyBits.py b/Server/webbyBits.py index ff78ec2..0b0ae9c 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -137,7 +137,7 @@ def playQueuedSongs(): # check for new songs every second # 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) + socketio.sleep(1) @socketio.on("connect") def handleConnect(): From 0099f17d3463b5654abe877fc02237fb2fb29c4b Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Sun, 8 Mar 2026 13:27:17 -0400 Subject: [PATCH 08/19] update the total song length from the socket messages --- Server/static/scripts.js | 3 ++- Server/webbyBits.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Server/static/scripts.js b/Server/static/scripts.js index 2c441b9..6871635 100644 --- a/Server/static/scripts.js +++ b/Server/static/scripts.js @@ -664,7 +664,8 @@ socket.on("timeUpdate", function(data) { // console.log("recieved data from timeUpdate"); // console.log(data); playlistElapsedSeconds = data["elapsedTime"]; - currentlyPlaying = data["playingState"] + currentlyPlaying = data["playingState"]; + playlistSongLength = data["songLength"] }); socket.on("skipSong",() => { diff --git a/Server/webbyBits.py b/Server/webbyBits.py index 0b0ae9c..710180e 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -102,7 +102,7 @@ def playQueuedSongs(): counter+=1 if(counter > 2): playingState = str(player.get_state()) == "State.Playing" - socketio.emit('timeUpdate',{"elapsedTime":player.get_time()/1000,"playingState":playingState}) + socketio.emit('timeUpdate',{"elapsedTime":player.get_time()/1000,"playingState":playingState,"songLength":player.get_length()/1000}) counter = 0 playerState = str(player.get_state()) endStates = ["State.Ended","State.Stopped","State.NothingSpecial"] From b31982153f0c6c9c301e2b70992ad1cdc2be5d4d Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:08:20 -0400 Subject: [PATCH 09/19] Added getting, starting on RESTful --- Server/static/scripts.js | 65 ++++++++++++++++++-------- Server/webbyBits.py | 99 +++++++++++++++++++--------------------- wishlist.md | 4 ++ 3 files changed, 99 insertions(+), 69 deletions(-) diff --git a/Server/static/scripts.js b/Server/static/scripts.js index 6871635..6b1e019 100644 --- a/Server/static/scripts.js +++ b/Server/static/scripts.js @@ -40,14 +40,42 @@ async function alertText(text="Song Added!") { alertbox.innerHTML = "" } } + +async function getFromServer(source,headersIn={},secure = false, password=adminPass) { + try { + let href = ""; + if(secure) { + href = "https://"+ip+"/" + source; + } else { + href = "http://"+ip+"/" + source; + } + headersIn["Jukebox-Auth"] = password; + headersIn["Accept"] = "application/json"; + let response = await fetch(href,{ + method:"GET", + headers:headersIn, + }) + let data = await response.json(); + if (response.status == ERR_NO_ADMIN) { + alertText("Error: Admin restricted action") + } else if(!response.ok){ + throw new 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) { + alertText(e); + } + +} + // a lot of this is kinda waffly because i was trying to get // it to return the right stuff and javascript is asyrcronouse (boo) -async function getFromServer(bodyInfo, source="", secure=false, password=adminPass) { +async function postFromServer(bodyInfo, source="", secure=false, password=adminPass) { try{ - if (bodyInfo != null) { - // the currently set password is always included in every request - bodyInfo["password"] = password; - } let href = ""; if(secure) { href = "https://"+ip+"/" + source; @@ -58,7 +86,8 @@ async function getFromServer(bodyInfo, source="", secure=false, password=adminPa method: "POST", body: JSON.stringify(bodyInfo), headers: { - "Content-type": "application/json; charset=UTF-8" + "Content-type": "application/json; charset=UTF-8", + "Jukebox-Auth": password } }); @@ -111,12 +140,12 @@ function getCookie(cname) { // also someone who likes things not being dumb more than me would have separated the client and server buttons async function controlButton(buttonType) { if (buttonType == "pp") { // Play-Pause button - let result = await getFromServer({control: "play-pause"}, "controls"); + let result = await postFromServer({control: "play-pause"}, "controls"); // console.log(result); currentlyPlaying = result["data"]["playingState"]; } else if (buttonType == "sk") { // Skip button // clearInterval(playlistTimeTimer); - let returnCode = await getFromServer({control: "skip"}, "controls"); + let returnCode = await postFromServer({control: "skip"}, "controls"); // console.log(returnCode["ok"]) if(returnCode["ok"]) { if (document.getElementById("playlist-mode").style.display == "block") { @@ -149,7 +178,7 @@ async function controlButton(buttonType) { document.getElementById("settings-mode").style.display = "block"; checkSettings() } else if (buttonType == "pm") { //Partymode toggle (in settings) - let response = await getFromServer({setting: "partymode-toggle"}, "settings") + let response = await postFromServer({setting: "partymode-toggle"}, "settings") if(response.ok) { justChangedSetting = true; checkSettings(); @@ -171,7 +200,7 @@ function searchSongsEnter(e) { async function searchSongs(searchTerm){ document.getElementById("songlist").innerHTML = "" - let fetchResults = await getFromServer({search:searchTerm},"search").then(); + let fetchResults = await getFromServer("search?query="+searchTerm); let searchResults = fetchResults.data; //generate the visual song list for(var fileName in searchResults) { @@ -229,7 +258,7 @@ function alertTimeSet(time) { } function qrCodeGenerate() { - let tempURL = "http://" + document.location.href.split("/")[2] + "/?ip=" + ip; + let tempURL = "http://" + URL.parse(document.location.href).host document.getElementById("qrcode").innerHTML = ""; // get the current foreground and background let dark = window.getComputedStyle(document.body).getPropertyValue("--text-color"); @@ -304,7 +333,7 @@ async function checkSettings(skipServer=false) { } } //ping the server here - data = await getFromServer({setting: "getsettings"}, "settings"); + data = await getFromServer("settings"); x = data["data"]; if (!(skipServer) || partyButtonState=="N/A") { if (x["partymode"] == false) { @@ -401,7 +430,7 @@ async function skipInPlaylist() { async function generateVisualPlaylist(conditions="") { document.getElementById("playlist").innerHTML = "

"; - data = await getFromServer(null, "playlist"); + data = await getFromServer("playlist"); playlist = data["data"]["playlist"]; currentlyPlaying = data["data"]["playingState"] playlist = Object.values(playlist).map(obj => { @@ -479,9 +508,9 @@ async function generateVisualPlaylist(conditions="") { } async function submitSong(songid) { - let returncode = await getFromServer({song: songid}, "songadd"); + let returncode = await postFromServer({song: songid}, "songadd"); if(returncode["status"] === ERR_NO_ADMIN) { - // right now the error is alerted in getFromServer, maybe will change that + // right now the error is alerted in postFromServer, maybe will change that } else if(returncode["status"]!==200) { alertText("That song's already in the queue! Hang on!") } else { @@ -555,7 +584,7 @@ async function submitPerms(e) { tempData["PM"] = document.getElementById("partymodesettingcheckbox").checked; tempData["VOL"] = document.getElementById("volumechangesettingcheckbox").checked; tempData["DUP"] = document.getElementById("duplicateallowesettingcheckbox").checked; - let returncode = await getFromServer({"setting":"perms","admin":tempData},"settings"); + let returncode = await postFromServer({"setting":"perms","admin":tempData},"settings"); if (!(returncode["ok"])) { // if you aren't allowed to check the box then toggle it again // its not perfect if you spam click, but it gets the point across to the user @@ -567,7 +596,7 @@ async function submitPerms(e) { } async function clearPlaylist() { - let returncode = await getFromServer({control:"clear"},"controls"); + let returncode = await postFromServer({control:"clear"},"controls"); if(returncode["status"] === ERR_NO_ADMIN || returncode == null) { // alertText("Admin Restricted ") // there's an admin restrict alert built into getFromServer @@ -590,7 +619,7 @@ document.getElementById("settings-mode").style.display = "none"; document.getElementById("volumerange").onchange = async function(e) { // there is no reason for this not to be a defined function // FIX THIS - let returnValue = await getFromServer({setting:"volume",level:e.target.value}, "settings") + let returnValue = await postFromServer({setting:"volume",level:e.target.value}, "settings") if (returnValue["status"] == ERR_NO_ADMIN) { // alertText("Error: Admin restricted action"); // there's an admin restrict alert built into getFromServer diff --git a/Server/webbyBits.py b/Server/webbyBits.py index 710180e..e980e62 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -33,7 +33,7 @@ controlPerms = { "AS":True, "PM":True, "VOL":True, - "DUP":True # Not implemented, allow duplicate songs in queue + "DUP":True } fileofDB = sql.connect("songDatabase.db") @@ -155,7 +155,7 @@ def playerControls(): recieveData=request.get_json(force=True) try: if recieveData["control"] == "play-pause": - if ADMIN_PASS == recieveData['password'] or controlPerms["PP"]: + if ADMIN_PASS == request.headers["Jukebox-Auth"] or controlPerms["PP"]: playingState = str(player.get_state())=="State.Playing" player.pause() return {"error":"ok","data":{"playingState":not(playingState)}},200 @@ -163,14 +163,14 @@ def playerControls(): playingState = str(player.get_state())=="State.Playing" return {"error":"Admin Restricted Action","data":{"playingState":playingState}},401 elif recieveData["control"] == "skip": - if ADMIN_PASS == recieveData['password'] or controlPerms["SK"]: + if ADMIN_PASS == request.headers["Jukebox-Auth"] or controlPerms["SK"]: skipNow = True 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 + if ADMIN_PASS == request.headers["Jukebox-Auth"]: # this is only ever allowed with the adminpassword with playlistLock: playlist.clear() return ERR_200 @@ -181,64 +181,64 @@ def playerControls(): except KeyError: return ERR_MISSING_ARGS -@app.route("/settings", methods=['POST']) +@app.route("/settings", methods=['POST','GET']) def settingsControl(): global controlPerms # set the volume and partymode global partyMode global player - recieveData = request.get_json(force=True) - 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) - if(volumePassed == 0): - # only emit a signal i the volume really changed - socketio.emit("settingsChange",{"settingToChange":"volume","newData":volumeLevel}) - return {"error":"ok","data":{"volumePassed":volumePassed}},200 + if (request.method == 'GET'): + return {"error":"ok","data":{"partymode":partyMode,"volume":player.audio_get_volume(),"admin":controlPerms}},200 + elif (request.method == 'POST'): + recieveData = request.get_json(force=True) + try: + if recieveData["setting"] == "volume": + if ADMIN_PASS == request.headers["Jukebox-Auth"] or controlPerms["VOL"]: + volumeLevel = int(recieveData["level"]) + if(volumeLevel <= 100 and volumeLevel >= 0): + volumePassed = player.audio_set_volume(volumeLevel) + if(volumePassed == 0): + # only emit a signal i the volume really changed + socketio.emit("settingsChange",{"settingToChange":"volume","newData":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 == request.headers["Jukebox-Auth"] or controlPerms["PM"]: + partyMode = not(partyMode) + partyModeStr = "On" if partyMode else "Off" + socketio.emit("settingsChange",{"settingToChange":"partymode","newData":partyModeStr}) + return ERR_200 + else: + return ERR_NO_ADMIN + elif recieveData["setting"] == "perms": + if ADMIN_PASS == request.headers["Jukebox-Auth"]: + controlPerms = recieveData["admin"] + # print(recieveData["admin"]) + socketio.emit("settingsChange",{"settingToChange":"perms","newData":controlPerms}) + return ERR_200 + else: + return ERR_NO_ADMIN else: - return ERR_NO_ADMIN - elif recieveData["setting"] == "partymode-toggle": - if ADMIN_PASS == recieveData['password'] or controlPerms["PM"]: - partyMode = not(partyMode) - partyModeStr = "On" if partyMode else "Off" - socketio.emit("settingsChange",{"settingToChange":"partymode","newData":partyModeStr}) - return ERR_200 - else: - return ERR_NO_ADMIN - elif recieveData["setting"] == "perms": - if ADMIN_PASS == recieveData["password"]: - controlPerms = recieveData["admin"] - # print(recieveData["admin"]) - socketio.emit("settingsChange",{"settingToChange":"perms","newData":controlPerms}) - 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},400 - except: - return ERR_MISSING_ARGS + return {"error":"Not a valid setting","data":None},400 + except Exception as e: + return {"error":e,"data":None},500 -@app.route("/search", methods=['POST']) +@app.route("/search", methods=['GET']) def searchSongDB(): - recieveData=request.get_json(force=True) + recieveData = request.args.get("query") fileofDB = sql.connect("songDatabase.db") songDatabase = fileofDB.cursor() try: results = [] # print(recieveData["search"]) - if (recieveData['search'] == ""): + if (recieveData == None or recieveData == ""): songDatabase.execute("SELECT * FROM virtualSongs") results = songDatabase.fetchall() else: - songDatabase.execute("SELECT * FROM virtualSongs WHERE virtualSongs MATCH ?",['"' + recieveData['search']+'"']) + songDatabase.execute("SELECT * FROM virtualSongs WHERE virtualSongs MATCH ?",['"' + recieveData +'"']) results = songDatabase.fetchall() tempdata = {} # this is a temporary solution so i dont have to change the client @@ -253,9 +253,6 @@ def searchSongDB(): fileofDB.close() return {"error":"ok","data":tempdata},200 - except KeyError: - fileofDB.close() - return ERR_MISSING_ARGS except sql.OperationalError as e: print(e) fileofDB.close() @@ -266,9 +263,9 @@ def searchSongDB(): def songadd(): recieveData=request.get_json(force=True) try: - if (ADMIN_PASS == recieveData['password']) or controlPerms["AS"]: + if (ADMIN_PASS == request.headers["Jukebox-Auth"]) or controlPerms["AS"]: # Password exists and is correct, or it's not restricted - if not(controlPerms["DUP"]) and (recieveData['song'] in playlist) and not(ADMIN_PASS == recieveData['password']): + if not(controlPerms["DUP"]) and (recieveData['song'] in playlist) and not(ADMIN_PASS == request.headers["Jukebox-Auth"]): return {"error":"This song is already in the queue, hang on!","data":None},409 else: queueSong(recieveData['song']) @@ -280,7 +277,7 @@ def songadd(): print(e) return ERR_MISSING_ARGS -@app.route("/playlist", methods=["POST"]) +@app.route("/playlist", methods=["GET"]) def getPlaylist(): global songNext fileofDB = sql.connect("songDatabase.db") diff --git a/wishlist.md b/wishlist.md index 6b1cbe5..77a58ad 100644 --- a/wishlist.md +++ b/wishlist.md @@ -1,5 +1,9 @@ # Wishlist *Features I would like to add, will be completed in any order* +- [ ] RESTful design + - [ ] Change all necesary POSTs to GET,DELETE (probably not PUT) + - [ ] Write an api layout + - Despite the fact this isn't necesary (nobody but the app should access any url but root) it's probably still a good idea. It's going to be very simple anyway - [ ] Loading indicator while awaiting server stuff - [ ] Refactoring existing code - [x] Remove old comments From ff09e518b81bfd4490379279517b86d9d62a46fe Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:30:59 -0400 Subject: [PATCH 10/19] Minor updates --- Server/webbyBits.py | 10 ++++------ wishlist.md | 1 + 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Server/webbyBits.py b/Server/webbyBits.py index e980e62..aafb6ee 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -26,7 +26,7 @@ else: ADMIN_PASS = hashlib.sha256(bytes(tempPass,'utf-8')).hexdigest() # True = everyone, False = admin only. Change in client while in use. -# play-pause,skip,addsong,partymode,volume in order +# play-pause,skip,addsong,partymode,volume,add duplicates in order controlPerms = { "PP":True, "SK":True, @@ -48,9 +48,6 @@ elif "/" in soundLocation: soundLocation += "/" else: soundLocation += "\\" -#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 - #Initializing all the global stuff random.seed() global partyMode @@ -223,8 +220,9 @@ def settingsControl(): return ERR_NO_ADMIN else: return {"error":"Not a valid setting","data":None},400 - except Exception as e: - return {"error":e,"data":None},500 + except KeyError as e: + print(f"Error: {e}") + return {"error":"Incorrect Data Sent","data":None},400 @app.route("/search", methods=['GET']) def searchSongDB(): diff --git a/wishlist.md b/wishlist.md index 77a58ad..691fa80 100644 --- a/wishlist.md +++ b/wishlist.md @@ -1,5 +1,6 @@ # Wishlist *Features I would like to add, will be completed in any order* +- [ ] Pages of song results, to avoid very long pages and large data being sent - [ ] RESTful design - [ ] Change all necesary POSTs to GET,DELETE (probably not PUT) - [ ] Write an api layout From 2d70c5306e5dd3618380b95e3c1e7c6dd19528d1 Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:34:25 -0400 Subject: [PATCH 11/19] Cleanup: Move JS Files Locally Moved remote JS files to local, (all under MIT liscense (i hope)) Added version number, plans for semantic versioning and better 'stable' releases --- Server/static/ext/popper.js | 134 ----------------------------- Server/static/ext/sha256.min.js | 9 ++ Server/static/ext/socket.io.min.js | 7 ++ Server/static/styles.css | 1 - Server/templates/index.html | 10 +-- Server/webbyBits.py | 15 ++-- 6 files changed, 29 insertions(+), 147 deletions(-) delete mode 100644 Server/static/ext/popper.js create mode 100644 Server/static/ext/sha256.min.js create mode 100644 Server/static/ext/socket.io.min.js diff --git a/Server/static/ext/popper.js b/Server/static/ext/popper.js deleted file mode 100644 index 19a42b9..0000000 --- a/Server/static/ext/popper.js +++ /dev/null @@ -1,134 +0,0 @@ -function Pop() { - // var cssRuleFile = "/src/css/style.css"; // will be the link once this css file became available online - var cssRuleFile = "https://cookieconsent.popupsmart.com/src/css/style.css"; // will be the link once this css file became available online - - let lnk = document.createElement("link"); - lnk.setAttribute("rel", "stylesheet"); - lnk.setAttribute("type", "text/css"); - lnk.setAttribute("href", cssRuleFile); - document.getElementsByTagName("head")[0].appendChild(lnk); - - let styl = "undefined"; - var conDivObj; - - var fadeInTime = 10; // If needed could be served as an customizable option to the user - var fadeOutTime = 10; - - let cookie = { - name: "cookieconsent_status", - path: "/", - expiryDays: 365 * 24 * 60 * 60 * 5000, - }; - - let content = { - /// Add a field for link color - message: - "This website uses cookies to ensure you get the best experience on our website.", - btnText: "Got it!", - mode: " banner bottom", - theme: " theme-classic", - palette: " palette1", - link: "Learn more", - href: "https://www.cookiesandyou.com", - target: "_blank", - }; - - let createPopUp = function () { - console.log(content); - if (typeof conDivObj === "undefined") { - conDivObj = document.createElement("DIV"); - conDivObj.style.opacity = 0; - conDivObj.setAttribute("id", "spopupCont"); - } - conDivObj.innerHTML = - '
' + - content.message + - '" + - content.link + - 'Powered by Popupsmart
'; - - document.body.appendChild(conDivObj); - fadeIn(conDivObj); - - document - .getElementById("cookie-btn") - .addEventListener("click", function () { - saveCookie(); - fadeOut(conDivObj); - }); - }; - - let fadeOut = function (element) { - var op = 1; - var timer = setInterval(function () { - if (op <= 0.1) { - clearInterval(timer); - conDivObj.parentElement.removeChild(conDivObj); - } - element.style.opacity = op; - element.style.filter = "alpha(opacity=" + op * 100 + ")"; - op -= op * 0.1; - }, fadeOutTime); - }; - let fadeIn = function (element) { - var op = 0.1; - var timer = setInterval(function () { - if (op >= 1) { - clearInterval(timer); - } - element.style.opacity = op; - element.style.filter = "alpha(opacity=" + op * 100 + ")"; - op += op * 0.1; - }, fadeInTime); - }; - - let checkCookie = function (key) { - var keyValue = document.cookie.match("(^|;) ?" + key + "=([^;]*)(;|$)"); - return keyValue ? true : false; - }; - - let saveCookie = function () { - var expires = new Date(); - expires.setTime(expires.getTime() + cookie.expiryDays); - document.cookie = - cookie.name + - "=" + - "ok" + - ";expires=" + - expires.toUTCString() + - "path=" + - cookie.path; - }; - - this.init = function (param) { - if (checkCookie(cookie.name)) return; - - if (typeof param === "object") { - if ("ButtonText" in param) content.btnText = param.ButtonText; - if ("Mode" in param) content.mode = " " + param.Mode; - if ("Theme" in param) content.theme = " " + param.Theme; - if ("Palette" in param) content.palette = " " + param.Palette; - if ("Message" in param) content.message = param.Message; - if ("LinkText" in param) content.link = param.LinkText; - if ("Location" in param) content.href = param.Location; - if ("Target" in param) content.target = param.Target; - if ("Time" in param) - setTimeout(function () { - createPopUp(); - }, param.Time * 1000); - else createPopUp(); - } - }; -} -window.start = new Pop(); diff --git a/Server/static/ext/sha256.min.js b/Server/static/ext/sha256.min.js new file mode 100644 index 0000000..7fe9102 --- /dev/null +++ b/Server/static/ext/sha256.min.js @@ -0,0 +1,9 @@ +/** + * [js-sha256]{@link https://github.com/emn178/js-sha256} + * + * @version 0.11.0 + * @author Chen, Yi-Cyuan [emn178@gmail.com] + * @copyright Chen, Yi-Cyuan 2014-2024 + * @license MIT + */ +!function(){"use strict";function t(t,i){i?(d[0]=d[16]=d[1]=d[2]=d[3]=d[4]=d[5]=d[6]=d[7]=d[8]=d[9]=d[10]=d[11]=d[12]=d[13]=d[14]=d[15]=0,this.blocks=d):this.blocks=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],t?(this.h0=3238371032,this.h1=914150663,this.h2=812702999,this.h3=4144912697,this.h4=4290775857,this.h5=1750603025,this.h6=1694076839,this.h7=3204075428):(this.h0=1779033703,this.h1=3144134277,this.h2=1013904242,this.h3=2773480762,this.h4=1359893119,this.h5=2600822924,this.h6=528734635,this.h7=1541459225),this.block=this.start=this.bytes=this.hBytes=0,this.finalized=this.hashed=!1,this.first=!0,this.is224=t}function i(i,r,s){var e,n=typeof i;if("string"===n){var o,a=[],u=i.length,c=0;for(e=0;e>>6,a[c++]=128|63&o):o<55296||o>=57344?(a[c++]=224|o>>>12,a[c++]=128|o>>>6&63,a[c++]=128|63&o):(o=65536+((1023&o)<<10|1023&i.charCodeAt(++e)),a[c++]=240|o>>>18,a[c++]=128|o>>>12&63,a[c++]=128|o>>>6&63,a[c++]=128|63&o);i=a}else{if("object"!==n)throw new Error(h);if(null===i)throw new Error(h);if(f&&i.constructor===ArrayBuffer)i=new Uint8Array(i);else if(!(Array.isArray(i)||f&&ArrayBuffer.isView(i)))throw new Error(h)}i.length>64&&(i=new t(r,!0).update(i).array());var y=[],p=[];for(e=0;e<64;++e){var l=i[e]||0;y[e]=92^l,p[e]=54^l}t.call(this,r,s),this.update(p),this.oKeyPad=y,this.inner=!0,this.sharedMemory=s}var h="input is invalid type",r="object"==typeof window,s=r?window:{};s.JS_SHA256_NO_WINDOW&&(r=!1);var e=!r&&"object"==typeof self,n=!s.JS_SHA256_NO_NODE_JS&&"object"==typeof process&&process.versions&&process.versions.node;n?s=global:e&&(s=self);var o=!s.JS_SHA256_NO_COMMON_JS&&"object"==typeof module&&module.exports,a="function"==typeof define&&define.amd,f=!s.JS_SHA256_NO_ARRAY_BUFFER&&"undefined"!=typeof ArrayBuffer,u="0123456789abcdef".split(""),c=[-2147483648,8388608,32768,128],y=[24,16,8,0],p=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298],l=["hex","array","digest","arrayBuffer"],d=[];!s.JS_SHA256_NO_NODE_JS&&Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),!f||!s.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW&&ArrayBuffer.isView||(ArrayBuffer.isView=function(t){return"object"==typeof t&&t.buffer&&t.buffer.constructor===ArrayBuffer});var A=function(i,h){return function(r){return new t(h,!0).update(r)[i]()}},w=function(i){var h=A("hex",i);n&&(h=b(h,i)),h.create=function(){return new t(i)},h.update=function(t){return h.create().update(t)};for(var r=0;r>>2]|=t[n]<>>2]|=s<>>2]|=(192|s>>>6)<>>2]|=(128|63&s)<=57344?(a[e>>>2]|=(224|s>>>12)<>>2]|=(128|s>>>6&63)<>>2]|=(128|63&s)<>>2]|=(240|s>>>18)<>>2]|=(128|s>>>12&63)<>>2]|=(128|s>>>6&63)<>>2]|=(128|63&s)<=64?(this.block=a[16],this.start=e-64,this.hash(),this.hashed=!0):this.start=e}return this.bytes>4294967295&&(this.hBytes+=this.bytes/4294967296<<0,this.bytes=this.bytes%4294967296),this}},t.prototype.finalize=function(){if(!this.finalized){this.finalized=!0;var t=this.blocks,i=this.lastByteIndex;t[16]=this.block,t[i>>>2]|=c[3&i],this.block=t[16],i>=56&&(this.hashed||this.hash(),t[0]=this.block,t[16]=t[1]=t[2]=t[3]=t[4]=t[5]=t[6]=t[7]=t[8]=t[9]=t[10]=t[11]=t[12]=t[13]=t[14]=t[15]=0),t[14]=this.hBytes<<3|this.bytes>>>29,t[15]=this.bytes<<3,this.hash()}},t.prototype.hash=function(){var t,i,h,r,s,e,n,o,a,f=this.h0,u=this.h1,c=this.h2,y=this.h3,l=this.h4,d=this.h5,A=this.h6,w=this.h7,b=this.blocks;for(t=16;t<64;++t)i=((s=b[t-15])>>>7|s<<25)^(s>>>18|s<<14)^s>>>3,h=((s=b[t-2])>>>17|s<<15)^(s>>>19|s<<13)^s>>>10,b[t]=b[t-16]+i+b[t-7]+h<<0;for(a=u&c,t=0;t<64;t+=4)this.first?(this.is224?(e=300032,w=(s=b[0]-1413257819)-150054599<<0,y=s+24177077<<0):(e=704751109,w=(s=b[0]-210244248)-1521486534<<0,y=s+143694565<<0),this.first=!1):(i=(f>>>2|f<<30)^(f>>>13|f<<19)^(f>>>22|f<<10),r=(e=f&u)^f&c^a,w=y+(s=w+(h=(l>>>6|l<<26)^(l>>>11|l<<21)^(l>>>25|l<<7))+(l&d^~l&A)+p[t]+b[t])<<0,y=s+(i+r)<<0),i=(y>>>2|y<<30)^(y>>>13|y<<19)^(y>>>22|y<<10),r=(n=y&f)^y&u^e,A=c+(s=A+(h=(w>>>6|w<<26)^(w>>>11|w<<21)^(w>>>25|w<<7))+(w&l^~w&d)+p[t+1]+b[t+1])<<0,i=((c=s+(i+r)<<0)>>>2|c<<30)^(c>>>13|c<<19)^(c>>>22|c<<10),r=(o=c&y)^c&f^n,d=u+(s=d+(h=(A>>>6|A<<26)^(A>>>11|A<<21)^(A>>>25|A<<7))+(A&w^~A&l)+p[t+2]+b[t+2])<<0,i=((u=s+(i+r)<<0)>>>2|u<<30)^(u>>>13|u<<19)^(u>>>22|u<<10),r=(a=u&c)^u&y^o,l=f+(s=l+(h=(d>>>6|d<<26)^(d>>>11|d<<21)^(d>>>25|d<<7))+(d&A^~d&w)+p[t+3]+b[t+3])<<0,f=s+(i+r)<<0,this.chromeBugWorkAround=!0;this.h0=this.h0+f<<0,this.h1=this.h1+u<<0,this.h2=this.h2+c<<0,this.h3=this.h3+y<<0,this.h4=this.h4+l<<0,this.h5=this.h5+d<<0,this.h6=this.h6+A<<0,this.h7=this.h7+w<<0},t.prototype.hex=function(){this.finalize();var t=this.h0,i=this.h1,h=this.h2,r=this.h3,s=this.h4,e=this.h5,n=this.h6,o=this.h7,a=u[t>>>28&15]+u[t>>>24&15]+u[t>>>20&15]+u[t>>>16&15]+u[t>>>12&15]+u[t>>>8&15]+u[t>>>4&15]+u[15&t]+u[i>>>28&15]+u[i>>>24&15]+u[i>>>20&15]+u[i>>>16&15]+u[i>>>12&15]+u[i>>>8&15]+u[i>>>4&15]+u[15&i]+u[h>>>28&15]+u[h>>>24&15]+u[h>>>20&15]+u[h>>>16&15]+u[h>>>12&15]+u[h>>>8&15]+u[h>>>4&15]+u[15&h]+u[r>>>28&15]+u[r>>>24&15]+u[r>>>20&15]+u[r>>>16&15]+u[r>>>12&15]+u[r>>>8&15]+u[r>>>4&15]+u[15&r]+u[s>>>28&15]+u[s>>>24&15]+u[s>>>20&15]+u[s>>>16&15]+u[s>>>12&15]+u[s>>>8&15]+u[s>>>4&15]+u[15&s]+u[e>>>28&15]+u[e>>>24&15]+u[e>>>20&15]+u[e>>>16&15]+u[e>>>12&15]+u[e>>>8&15]+u[e>>>4&15]+u[15&e]+u[n>>>28&15]+u[n>>>24&15]+u[n>>>20&15]+u[n>>>16&15]+u[n>>>12&15]+u[n>>>8&15]+u[n>>>4&15]+u[15&n];return this.is224||(a+=u[o>>>28&15]+u[o>>>24&15]+u[o>>>20&15]+u[o>>>16&15]+u[o>>>12&15]+u[o>>>8&15]+u[o>>>4&15]+u[15&o]),a},t.prototype.toString=t.prototype.hex,t.prototype.digest=function(){this.finalize();var t=this.h0,i=this.h1,h=this.h2,r=this.h3,s=this.h4,e=this.h5,n=this.h6,o=this.h7,a=[t>>>24&255,t>>>16&255,t>>>8&255,255&t,i>>>24&255,i>>>16&255,i>>>8&255,255&i,h>>>24&255,h>>>16&255,h>>>8&255,255&h,r>>>24&255,r>>>16&255,r>>>8&255,255&r,s>>>24&255,s>>>16&255,s>>>8&255,255&s,e>>>24&255,e>>>16&255,e>>>8&255,255&e,n>>>24&255,n>>>16&255,n>>>8&255,255&n];return this.is224||a.push(o>>>24&255,o>>>16&255,o>>>8&255,255&o),a},t.prototype.array=t.prototype.digest,t.prototype.arrayBuffer=function(){this.finalize();var t=new ArrayBuffer(this.is224?28:32),i=new DataView(t);return i.setUint32(0,this.h0),i.setUint32(4,this.h1),i.setUint32(8,this.h2),i.setUint32(12,this.h3),i.setUint32(16,this.h4),i.setUint32(20,this.h5),i.setUint32(24,this.h6),this.is224||i.setUint32(28,this.h7),t},(i.prototype=new t).finalize=function(){if(t.prototype.finalize.call(this),this.inner){this.inner=!1;var i=this.array();t.call(this,this.is224,this.sharedMemory),this.update(this.oKeyPad),this.update(i),t.prototype.finalize.call(this)}};var B=w();B.sha256=B,B.sha224=w(!0),B.sha256.hmac=v(),B.sha224.hmac=v(!0),o?module.exports=B:(s.sha256=B.sha256,s.sha224=B.sha224,a&&define(function(){return B}))}(); \ No newline at end of file diff --git a/Server/static/ext/socket.io.min.js b/Server/static/ext/socket.io.min.js new file mode 100644 index 0000000..d6b2d60 --- /dev/null +++ b/Server/static/ext/socket.io.min.js @@ -0,0 +1,7 @@ +/*! + * Socket.IO v4.7.5 + * (c) 2014-2024 Guillermo Rauch + * Released under the MIT License. + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).io=t()}(this,(function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,s=!0,a=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return s=e.done,e},e:function(e){a=!0,o=e},f:function(){try{s||null==n.return||n.return()}finally{if(a)throw o}}}}var v=Object.create(null);v.open="0",v.close="1",v.ping="2",v.pong="3",v.message="4",v.upgrade="5",v.noop="6";var g=Object.create(null);Object.keys(v).forEach((function(e){g[v[e]]=e}));var m,b={type:"error",data:"parser error"},k="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===Object.prototype.toString.call(Blob),w="function"==typeof ArrayBuffer,_=function(e){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer instanceof ArrayBuffer},E=function(e,t,n){var r=e.type,i=e.data;return k&&i instanceof Blob?t?n(i):A(i,n):w&&(i instanceof ArrayBuffer||_(i))?t?n(i):A(new Blob([i]),n):n(v[r]+(i||""))},A=function(e,t){var n=new FileReader;return n.onload=function(){var e=n.result.split(",")[1];t("b"+(e||""))},n.readAsDataURL(e)};function O(e){return e instanceof Uint8Array?e:e instanceof ArrayBuffer?new Uint8Array(e):new Uint8Array(e.buffer,e.byteOffset,e.byteLength)}for(var T="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",R="undefined"==typeof Uint8Array?[]:new Uint8Array(256),C=0;C<64;C++)R[T.charCodeAt(C)]=C;var B,S="function"==typeof ArrayBuffer,N=function(e,t){if("string"!=typeof e)return{type:"message",data:x(e,t)};var n=e.charAt(0);return"b"===n?{type:"message",data:L(e.substring(1),t)}:g[n]?e.length>1?{type:g[n],data:e.substring(1)}:{type:g[n]}:b},L=function(e,t){if(S){var n=function(e){var t,n,r,i,o,s=.75*e.length,a=e.length,c=0;"="===e[e.length-1]&&(s--,"="===e[e.length-2]&&s--);var u=new ArrayBuffer(s),h=new Uint8Array(u);for(t=0;t>4,h[c++]=(15&r)<<4|i>>2,h[c++]=(3&i)<<6|63&o;return u}(e);return x(n,t)}return{base64:!0,data:e}},x=function(e,t){return"blob"===t?e instanceof Blob?e:new Blob([e]):e instanceof ArrayBuffer?e:e.buffer},P=String.fromCharCode(30);function j(){return new TransformStream({transform:function(e,t){!function(e,t){k&&e.data instanceof Blob?e.data.arrayBuffer().then(O).then(t):w&&(e.data instanceof ArrayBuffer||_(e.data))?t(O(e.data)):E(e,!1,(function(e){m||(m=new TextEncoder),t(m.encode(e))}))}(e,(function(n){var r,i=n.length;if(i<126)r=new Uint8Array(1),new DataView(r.buffer).setUint8(0,i);else if(i<65536){r=new Uint8Array(3);var o=new DataView(r.buffer);o.setUint8(0,126),o.setUint16(1,i)}else{r=new Uint8Array(9);var s=new DataView(r.buffer);s.setUint8(0,127),s.setBigUint64(1,BigInt(i))}e.data&&"string"!=typeof e.data&&(r[0]|=128),t.enqueue(r),t.enqueue(n)}))}})}function q(e){return e.reduce((function(e,t){return e+t.length}),0)}function D(e,t){if(e[0].length===t)return e.shift();for(var n=new Uint8Array(t),r=0,i=0;i1?t-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{};return e+"://"+this._hostname()+this._port()+this.opts.path+this._query(t)}},{key:"_hostname",value:function(){var e=this.opts.hostname;return-1===e.indexOf(":")?e:"["+e+"]"}},{key:"_port",value:function(){return this.opts.port&&(this.opts.secure&&Number(443!==this.opts.port)||!this.opts.secure&&80!==Number(this.opts.port))?":"+this.opts.port:""}},{key:"_query",value:function(e){var t=function(e){var t="";for(var n in e)e.hasOwnProperty(n)&&(t.length&&(t+="&"),t+=encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return t}(e);return t.length?"?"+t:""}}]),i}(U),z="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_".split(""),J=64,$={},Q=0,X=0;function G(e){var t="";do{t=z[e%J]+t,e=Math.floor(e/J)}while(e>0);return t}function Z(){var e=G(+new Date);return e!==K?(Q=0,K=e):e+"."+G(Q++)}for(;X0&&void 0!==arguments[0]?arguments[0]:{};return i(e,{xd:this.xd,cookieJar:this.cookieJar},this.opts),new se(this.uri(),e)}},{key:"doWrite",value:function(e,t){var n=this,r=this.request({method:"POST",data:e});r.on("success",t),r.on("error",(function(e,t){n.onError("xhr post error",e,t)}))}},{key:"doPoll",value:function(){var e=this,t=this.request();t.on("data",this.onData.bind(this)),t.on("error",(function(t,n){e.onError("xhr poll error",t,n)})),this.pollXhr=t}}]),s}(W),se=function(e){o(i,e);var n=l(i);function i(e,r){var o;return t(this,i),H(f(o=n.call(this)),r),o.opts=r,o.method=r.method||"GET",o.uri=e,o.data=void 0!==r.data?r.data:null,o.create(),o}return r(i,[{key:"create",value:function(){var e,t=this,n=F(this.opts,"agent","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","autoUnref");n.xdomain=!!this.opts.xd;var r=this.xhr=new ne(n);try{r.open(this.method,this.uri,!0);try{if(this.opts.extraHeaders)for(var o in r.setDisableHeaderCheck&&r.setDisableHeaderCheck(!0),this.opts.extraHeaders)this.opts.extraHeaders.hasOwnProperty(o)&&r.setRequestHeader(o,this.opts.extraHeaders[o])}catch(e){}if("POST"===this.method)try{r.setRequestHeader("Content-type","text/plain;charset=UTF-8")}catch(e){}try{r.setRequestHeader("Accept","*/*")}catch(e){}null===(e=this.opts.cookieJar)||void 0===e||e.addCookies(r),"withCredentials"in r&&(r.withCredentials=this.opts.withCredentials),this.opts.requestTimeout&&(r.timeout=this.opts.requestTimeout),r.onreadystatechange=function(){var e;3===r.readyState&&(null===(e=t.opts.cookieJar)||void 0===e||e.parseCookies(r)),4===r.readyState&&(200===r.status||1223===r.status?t.onLoad():t.setTimeoutFn((function(){t.onError("number"==typeof r.status?r.status:0)}),0))},r.send(this.data)}catch(e){return void this.setTimeoutFn((function(){t.onError(e)}),0)}"undefined"!=typeof document&&(this.index=i.requestsCount++,i.requests[this.index]=this)}},{key:"onError",value:function(e){this.emitReserved("error",e,this.xhr),this.cleanup(!0)}},{key:"cleanup",value:function(e){if(void 0!==this.xhr&&null!==this.xhr){if(this.xhr.onreadystatechange=re,e)try{this.xhr.abort()}catch(e){}"undefined"!=typeof document&&delete i.requests[this.index],this.xhr=null}}},{key:"onLoad",value:function(){var e=this.xhr.responseText;null!==e&&(this.emitReserved("data",e),this.emitReserved("success"),this.cleanup())}},{key:"abort",value:function(){this.cleanup()}}]),i}(U);if(se.requestsCount=0,se.requests={},"undefined"!=typeof document)if("function"==typeof attachEvent)attachEvent("onunload",ae);else if("function"==typeof addEventListener){addEventListener("onpagehide"in I?"pagehide":"unload",ae,!1)}function ae(){for(var e in se.requests)se.requests.hasOwnProperty(e)&&se.requests[e].abort()}var ce="function"==typeof Promise&&"function"==typeof Promise.resolve?function(e){return Promise.resolve().then(e)}:function(e,t){return t(e,0)},ue=I.WebSocket||I.MozWebSocket,he="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),fe=function(e){o(i,e);var n=l(i);function i(e){var r;return t(this,i),(r=n.call(this,e)).supportsBinary=!e.forceBase64,r}return r(i,[{key:"name",get:function(){return"websocket"}},{key:"doOpen",value:function(){if(this.check()){var e=this.uri(),t=this.opts.protocols,n=he?{}:F(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(n.headers=this.opts.extraHeaders);try{this.ws=he?new ue(e,t,n):t?new ue(e,t):new ue(e)}catch(e){return this.emitReserved("error",e)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()}}},{key:"addEventListeners",value:function(){var e=this;this.ws.onopen=function(){e.opts.autoUnref&&e.ws._socket.unref(),e.onOpen()},this.ws.onclose=function(t){return e.onClose({description:"websocket connection closed",context:t})},this.ws.onmessage=function(t){return e.onData(t.data)},this.ws.onerror=function(t){return e.onError("websocket error",t)}}},{key:"write",value:function(e){var t=this;this.writable=!1;for(var n=function(){var n=e[r],i=r===e.length-1;E(n,t.supportsBinary,(function(e){try{t.ws.send(e)}catch(e){}i&&ce((function(){t.writable=!0,t.emitReserved("drain")}),t.setTimeoutFn)}))},r=0;rMath.pow(2,21)-1){a.enqueue(b);break}i=l*Math.pow(2,32)+f.getUint32(4),r=3}else{if(q(n)e){a.enqueue(b);break}}}})}(Number.MAX_SAFE_INTEGER,e.socket.binaryType),r=t.readable.pipeThrough(n).getReader(),i=j();i.readable.pipeTo(t.writable),e.writer=i.writable.getWriter();!function t(){r.read().then((function(n){var r=n.done,i=n.value;r||(e.onPacket(i),t())})).catch((function(e){}))}();var o={type:"open"};e.query.sid&&(o.data='{"sid":"'.concat(e.query.sid,'"}')),e.writer.write(o).then((function(){return e.onOpen()}))}))})))}},{key:"write",value:function(e){var t=this;this.writable=!1;for(var n=function(){var n=e[r],i=r===e.length-1;t.writer.write(n).then((function(){i&&ce((function(){t.writable=!0,t.emitReserved("drain")}),t.setTimeoutFn)}))},r=0;r1&&void 0!==arguments[1]?arguments[1]:{};return t(this,a),(r=s.call(this)).binaryType="arraybuffer",r.writeBuffer=[],n&&"object"===e(n)&&(o=n,n=null),n?(n=ve(n),o.hostname=n.host,o.secure="https"===n.protocol||"wss"===n.protocol,o.port=n.port,n.query&&(o.query=n.query)):o.host&&(o.hostname=ve(o.host).host),H(f(r),o),r.secure=null!=o.secure?o.secure:"undefined"!=typeof location&&"https:"===location.protocol,o.hostname&&!o.port&&(o.port=r.secure?"443":"80"),r.hostname=o.hostname||("undefined"!=typeof location?location.hostname:"localhost"),r.port=o.port||("undefined"!=typeof location&&location.port?location.port:r.secure?"443":"80"),r.transports=o.transports||["polling","websocket","webtransport"],r.writeBuffer=[],r.prevBufferLen=0,r.opts=i({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},o),r.opts.path=r.opts.path.replace(/\/$/,"")+(r.opts.addTrailingSlash?"/":""),"string"==typeof r.opts.query&&(r.opts.query=function(e){for(var t={},n=e.split("&"),r=0,i=n.length;r1))return this.writeBuffer;for(var e,t=1,n=0;n=57344?n+=3:(r++,n+=4);return n}(e):Math.ceil(1.33*(e.byteLength||e.size))),n>0&&t>this.maxPayload)return this.writeBuffer.slice(0,n);t+=2}return this.writeBuffer}},{key:"write",value:function(e,t,n){return this.sendPacket("message",e,t,n),this}},{key:"send",value:function(e,t,n){return this.sendPacket("message",e,t,n),this}},{key:"sendPacket",value:function(e,t,n,r){if("function"==typeof t&&(r=t,t=void 0),"function"==typeof n&&(r=n,n=null),"closing"!==this.readyState&&"closed"!==this.readyState){(n=n||{}).compress=!1!==n.compress;var i={type:e,data:t,options:n};this.emitReserved("packetCreate",i),this.writeBuffer.push(i),r&&this.once("flush",r),this.flush()}}},{key:"close",value:function(){var e=this,t=function(){e.onClose("forced close"),e.transport.close()},n=function n(){e.off("upgrade",n),e.off("upgradeError",n),t()},r=function(){e.once("upgrade",n),e.once("upgradeError",n)};return"opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(function(){e.upgrading?r():t()})):this.upgrading?r():t()),this}},{key:"onError",value:function(e){a.priorWebsocketSuccess=!1,this.emitReserved("error",e),this.onClose("transport error",e)}},{key:"onClose",value:function(e,t){"opening"!==this.readyState&&"open"!==this.readyState&&"closing"!==this.readyState||(this.clearTimeoutFn(this.pingTimeoutTimer),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),"function"==typeof removeEventListener&&(removeEventListener("beforeunload",this.beforeunloadEventListener,!1),removeEventListener("offline",this.offlineEventListener,!1)),this.readyState="closed",this.id=null,this.emitReserved("close",e,t),this.writeBuffer=[],this.prevBufferLen=0)}},{key:"filterUpgrades",value:function(e){for(var t=[],n=0,r=e.length;n=0&&t.num1?t-1:0),r=1;r1?n-1:0),i=1;in._opts.retries&&(n._queue.shift(),t&&t(e));else if(n._queue.shift(),t){for(var i=arguments.length,o=new Array(i>1?i-1:0),s=1;s0&&void 0!==arguments[0]&&arguments[0];if(this.connected&&0!==this._queue.length){var t=this._queue[0];t.pending&&!e||(t.pending=!0,t.tryCount++,this.flags=t.flags,this.emit.apply(this,t.args))}}},{key:"packet",value:function(e){e.nsp=this.nsp,this.io._packet(e)}},{key:"onopen",value:function(){var e=this;"function"==typeof this.auth?this.auth((function(t){e._sendConnectPacket(t)})):this._sendConnectPacket(this.auth)}},{key:"_sendConnectPacket",value:function(e){this.packet({type:Be.CONNECT,data:this._pid?i({pid:this._pid,offset:this._lastOffset},e):e})}},{key:"onerror",value:function(e){this.connected||this.emitReserved("connect_error",e)}},{key:"onclose",value:function(e,t){this.connected=!1,delete this.id,this.emitReserved("disconnect",e,t),this._clearAcks()}},{key:"_clearAcks",value:function(){var e=this;Object.keys(this.acks).forEach((function(t){if(!e.sendBuffer.some((function(e){return String(e.id)===t}))){var n=e.acks[t];delete e.acks[t],n.withError&&n.call(e,new Error("socket has been disconnected"))}}))}},{key:"onpacket",value:function(e){if(e.nsp===this.nsp)switch(e.type){case Be.CONNECT:e.data&&e.data.sid?this.onconnect(e.data.sid,e.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case Be.EVENT:case Be.BINARY_EVENT:this.onevent(e);break;case Be.ACK:case Be.BINARY_ACK:this.onack(e);break;case Be.DISCONNECT:this.ondisconnect();break;case Be.CONNECT_ERROR:this.destroy();var t=new Error(e.data.message);t.data=e.data.data,this.emitReserved("connect_error",t)}}},{key:"onevent",value:function(e){var t=e.data||[];null!=e.id&&t.push(this.ack(e.id)),this.connected?this.emitEvent(t):this.receiveBuffer.push(Object.freeze(t))}},{key:"emitEvent",value:function(e){if(this._anyListeners&&this._anyListeners.length){var t,n=y(this._anyListeners.slice());try{for(n.s();!(t=n.n()).done;){t.value.apply(this,e)}}catch(e){n.e(e)}finally{n.f()}}p(s(a.prototype),"emit",this).apply(this,e),this._pid&&e.length&&"string"==typeof e[e.length-1]&&(this._lastOffset=e[e.length-1])}},{key:"ack",value:function(e){var t=this,n=!1;return function(){if(!n){n=!0;for(var r=arguments.length,i=new Array(r),o=0;o0&&e.jitter<=1?e.jitter:0,this.attempts=0}Ie.prototype.duration=function(){var e=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var t=Math.random(),n=Math.floor(t*this.jitter*e);e=0==(1&Math.floor(10*t))?e-n:e+n}return 0|Math.min(e,this.max)},Ie.prototype.reset=function(){this.attempts=0},Ie.prototype.setMin=function(e){this.ms=e},Ie.prototype.setMax=function(e){this.max=e},Ie.prototype.setJitter=function(e){this.jitter=e};var Fe=function(n){o(s,n);var i=l(s);function s(n,r){var o,a;t(this,s),(o=i.call(this)).nsps={},o.subs=[],n&&"object"===e(n)&&(r=n,n=void 0),(r=r||{}).path=r.path||"/socket.io",o.opts=r,H(f(o),r),o.reconnection(!1!==r.reconnection),o.reconnectionAttempts(r.reconnectionAttempts||1/0),o.reconnectionDelay(r.reconnectionDelay||1e3),o.reconnectionDelayMax(r.reconnectionDelayMax||5e3),o.randomizationFactor(null!==(a=r.randomizationFactor)&&void 0!==a?a:.5),o.backoff=new Ie({min:o.reconnectionDelay(),max:o.reconnectionDelayMax(),jitter:o.randomizationFactor()}),o.timeout(null==r.timeout?2e4:r.timeout),o._readyState="closed",o.uri=n;var c=r.parser||je;return o.encoder=new c.Encoder,o.decoder=new c.Decoder,o._autoConnect=!1!==r.autoConnect,o._autoConnect&&o.open(),o}return r(s,[{key:"reconnection",value:function(e){return arguments.length?(this._reconnection=!!e,this):this._reconnection}},{key:"reconnectionAttempts",value:function(e){return void 0===e?this._reconnectionAttempts:(this._reconnectionAttempts=e,this)}},{key:"reconnectionDelay",value:function(e){var t;return void 0===e?this._reconnectionDelay:(this._reconnectionDelay=e,null===(t=this.backoff)||void 0===t||t.setMin(e),this)}},{key:"randomizationFactor",value:function(e){var t;return void 0===e?this._randomizationFactor:(this._randomizationFactor=e,null===(t=this.backoff)||void 0===t||t.setJitter(e),this)}},{key:"reconnectionDelayMax",value:function(e){var t;return void 0===e?this._reconnectionDelayMax:(this._reconnectionDelayMax=e,null===(t=this.backoff)||void 0===t||t.setMax(e),this)}},{key:"timeout",value:function(e){return arguments.length?(this._timeout=e,this):this._timeout}},{key:"maybeReconnectOnOpen",value:function(){!this._reconnecting&&this._reconnection&&0===this.backoff.attempts&&this.reconnect()}},{key:"open",value:function(e){var t=this;if(~this._readyState.indexOf("open"))return this;this.engine=new ge(this.uri,this.opts);var n=this.engine,r=this;this._readyState="opening",this.skipReconnect=!1;var i=qe(n,"open",(function(){r.onopen(),e&&e()})),o=function(n){t.cleanup(),t._readyState="closed",t.emitReserved("error",n),e?e(n):t.maybeReconnectOnOpen()},s=qe(n,"error",o);if(!1!==this._timeout){var a=this._timeout,c=this.setTimeoutFn((function(){i(),o(new Error("timeout")),n.close()}),a);this.opts.autoUnref&&c.unref(),this.subs.push((function(){t.clearTimeoutFn(c)}))}return this.subs.push(i),this.subs.push(s),this}},{key:"connect",value:function(e){return this.open(e)}},{key:"onopen",value:function(){this.cleanup(),this._readyState="open",this.emitReserved("open");var e=this.engine;this.subs.push(qe(e,"ping",this.onping.bind(this)),qe(e,"data",this.ondata.bind(this)),qe(e,"error",this.onerror.bind(this)),qe(e,"close",this.onclose.bind(this)),qe(this.decoder,"decoded",this.ondecoded.bind(this)))}},{key:"onping",value:function(){this.emitReserved("ping")}},{key:"ondata",value:function(e){try{this.decoder.add(e)}catch(e){this.onclose("parse error",e)}}},{key:"ondecoded",value:function(e){var t=this;ce((function(){t.emitReserved("packet",e)}),this.setTimeoutFn)}},{key:"onerror",value:function(e){this.emitReserved("error",e)}},{key:"socket",value:function(e,t){var n=this.nsps[e];return n?this._autoConnect&&!n.active&&n.connect():(n=new Ue(this,e,t),this.nsps[e]=n),n}},{key:"_destroy",value:function(e){for(var t=0,n=Object.keys(this.nsps);t=this._reconnectionAttempts)this.backoff.reset(),this.emitReserved("reconnect_failed"),this._reconnecting=!1;else{var n=this.backoff.duration();this._reconnecting=!0;var r=this.setTimeoutFn((function(){t.skipReconnect||(e.emitReserved("reconnect_attempt",t.backoff.attempts),t.skipReconnect||t.open((function(n){n?(t._reconnecting=!1,t.reconnect(),e.emitReserved("reconnect_error",n)):t.onreconnect()})))}),n);this.opts.autoUnref&&r.unref(),this.subs.push((function(){e.clearTimeoutFn(r)}))}}},{key:"onreconnect",value:function(){var e=this.backoff.attempts;this._reconnecting=!1,this.backoff.reset(),this.emitReserved("reconnect",e)}}]),s}(U),Me={};function Ve(t,n){"object"===e(t)&&(n=t,t=void 0);var r,i=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2?arguments[2]:void 0,r=e;n=n||"undefined"!=typeof location&&location,null==e&&(e=n.protocol+"//"+n.host),"string"==typeof e&&("/"===e.charAt(0)&&(e="/"===e.charAt(1)?n.protocol+e:n.host+e),/^(https?|wss?):\/\//.test(e)||(e=void 0!==n?n.protocol+"//"+e:"https://"+e),r=ve(e)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var i=-1!==r.host.indexOf(":")?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+i+":"+r.port+t,r.href=r.protocol+"://"+i+(n&&n.port===r.port?"":":"+r.port),r}(t,(n=n||{}).path||"/socket.io"),o=i.source,s=i.id,a=i.path,c=Me[s]&&a in Me[s].nsps;return n.forceNew||n["force new connection"]||!1===n.multiplex||c?r=new Fe(o,n):(Me[s]||(Me[s]=new Fe(o,n)),r=Me[s]),i.query&&!n.query&&(n.query=i.queryKey),r.socket(i.path,n)}return i(Ve,{Manager:Fe,Socket:Ue,io:Ve,connect:Ve}),Ve})); +//# sourceMappingURL=socket.io.min.js.map diff --git a/Server/static/styles.css b/Server/static/styles.css index 70bd991..f2e3e27 100644 --- a/Server/static/styles.css +++ b/Server/static/styles.css @@ -235,7 +235,6 @@ h4 { .versionNumber { font-size: 11px; - font-style: italic; text-align: left; width: 80%; } diff --git a/Server/templates/index.html b/Server/templates/index.html index e8d0eb9..8775d11 100644 --- a/Server/templates/index.html +++ b/Server/templates/index.html @@ -7,14 +7,11 @@ - + - + - - -

Jukebox Remote

Add songs to the shared playlist below!

@@ -115,7 +112,8 @@ changes visibility with JS-->

Wipe the playlist, except the currently playing song. With PartyMode enabled, a second song will be added back randomly

-

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.

+

Release {{ REL_VER_NUM }}

diff --git a/Server/webbyBits.py b/Server/webbyBits.py index aafb6ee..f8758f5 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -5,12 +5,14 @@ from flask import request,render_template from flask_cors import CORS from flask_socketio import SocketIO import sqlite3 as sql -import vlc,threading,time,random,argparse,dotenv,os,hashlib,string +import vlc,threading,random,argparse,dotenv,os,hashlib,string,getpass + +# So i'm famously bad at following Semantic versioning, we're gonna see how this goes +REL_VER_NUM = "0.0.1" # Argparse Stuff parser=argparse.ArgumentParser(description="Options for the Webby Bits") -# parser.add_argument('-p','--port',help="Port to host on, not the same as the web (client) port",default='19054') -parser.add_argument('-a','--admin',help="Add an admin password to be used in the client. DO NOT use a password you use elsewhere",default="") +parser.add_argument('-a','--admin',help="Set as True to be prompted to",default=False) args = parser.parse_args() dotenv.load_dotenv() portTheUserPicked=os.getenv("SERVER_PORT") @@ -18,8 +20,8 @@ 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() +if bool(args.admin) and args.admin.lower() != "false": + ADMIN_PASS = hashlib.sha256(bytes(getpass.getpass("Enter AdminPass: "),'utf-8')).hexdigest() else: tempPass = ''.join(random.choices(string.ascii_letters + string.digits +"?"+"!",k=20)) print("No adminPass was set, the auto generated one is: "+tempPass) @@ -142,7 +144,7 @@ def handleConnect(): @app.route("/",methods=['GET']) def returnStaticFile(): - return render_template("index.html") + return render_template("index.html",REL_VER_NUM=REL_VER_NUM) @app.route("/controls", methods=['POST']) def playerControls(): @@ -320,5 +322,6 @@ if __name__ == "__main__": queueThread = threading.Thread(target=playQueuedSongs) queueThread.daemon = True queueThread.start() + print(f"PartyJukebox v{REL_VER_NUM} running on port {portTheUserPicked}") socketio.run(app=app,host='0.0.0.0', port=portTheUserPicked) \ No newline at end of file From 1230ce60b3f68ae4b9fa614b8fde2fd866d1e538 Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:36:49 -0400 Subject: [PATCH 12/19] Adding files i forgot last time (new to command line git, sorry --- readme.md | 12 +++++++----- wishlist.md | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/readme.md b/readme.md index 2589f25..526a4a7 100644 --- a/readme.md +++ b/readme.md @@ -33,8 +33,9 @@ webbyBits.py * *If getting images, this process may take a long time with a large amount of mp3 files* 4. Run `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* + * *You can add an admin password at runtime with* `-a True` *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*** + * You will be prompted in console for a password to be used * 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 @@ -97,14 +98,15 @@ From left to right: The exact process of the password's plaintext scope is as follows -- On the server, you type in the password on the server in the console, the python script takes that plaintext, hashes it, then stores that hash as a variable. The plaintext is also technically a variable, but it's not accessed after that initial hashing. (It's also going to be visible in your console history) +- On the server, you type in the password on the server in the console, the python script takes that input directly, hashes it, then stores that hash as a variable. The plaintext could be in memory, but it's not accessible in the code after that initial hashing. If you typed your own password, it won't be visible in the console history after it is typed. -- 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) +- 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 input box, 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) -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. +None of this is "secure", but it's better than sending plaintext passwords, which is what I was doing before. Hypothetically somebody who intercepted any packets with 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) + - Socket.io: JS file found [here](https://cdn.socket.io/4.7.5/socket.io.min.js) + - SHA256 over http: JS file found [here](https://cdnjs.cloudflare.com/ajax/libs/js-sha256/0.11.0/sha256.min.js) *See `LICENSE.md` for redistribution and editing details.* diff --git a/wishlist.md b/wishlist.md index 691fa80..2602f75 100644 --- a/wishlist.md +++ b/wishlist.md @@ -9,11 +9,12 @@ - [ ] Refactoring existing code - [x] Remove old comments - [ ] Update the SQL -> Server -> Client pipeline when searching and building playlist + - Moving to mongo might make more sense to maintain a dictionary/JSON like format, but that needs a server - [ ] Verify all if-else sequences are correct and not redundant - [ ] Security Updates - [x] `.env` file for the api keys and other runtime info to be set, rather than in the `.py` files - [x] Hashing rather than plaintext sending passwords (that way at least the password text itself isn't transmitted over the network) - - [ ] Actually use SSL, for posting (CORS seems like an issue) + - [ ] Actually use TLS, for posting (CORS seems like an issue) - [ ] Accessibility - [ ] Better use of semantic HTML tags - [ ] Full keyboard control (tab, enter to select, tab between control buttons) From 89bf77304c995df62e16223865d85c229d18d4b7 Mon Sep 17 00:00:00 2001 From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:37:09 -0400 Subject: [PATCH 13/19] Update Readme for accuracy Making sure everything in the readme is relevant to the modern version this includes removing references to mp3s (now audio files) and to a seperate web client --- readme.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/readme.md b/readme.md index 526a4a7..6ccc66c 100644 --- a/readme.md +++ b/readme.md @@ -4,13 +4,12 @@ You can use `--help` on any of the python files to see all the properties that can be changed at runtime ## Purpose The **Party Jukebox** is a program that allows many people to add music, skip songs, play, and pause from any web device to the same device and playlist. \ -This was created for a personal use case for parties, and is a simple, (mostly) functional solution to have a collective playlist for local mp3 files. \ -The main advantage compared to doing something similar using Spotify is that you can limit the songs that can be played to your selection. Songs can be chosen, but only from a list. +This was created for a personal use case for parties, and is a simple solution to have a collective playlist for local audio files. \ +The main advantage compared to doing something similar using Spotify or another streaming service is that you can limit the songs that can be played to your selection. Songs can be chosen, but only from a list. ## Basic Setup ### Client Setup: -The client is a web application that can be hosted on any server, it need not be the same device running the music player. -* If the app is being setup for a large group, you can distribute the url (via QR code, for example) with `?ip=YOURSERVERHOSTNAME:19054` set as an attribute after the url. -* You can also add `?darkmode=(true/false)` to set the default colour scheme, but this will be overwritten by the users saved choice in the cookie if they change it themselves +The client is a web application hosted by the flask app running the audio player. +* You can add `?darkmode=(true/false)` to the client URL to set the default colour scheme, but this will be overwritten by the user's saved choice if they change it themselves ### Server Setup: **Pre-setup:** If you want the songs to have art associated with them, it is all hosted on and retrieved from LastFM, and you will need to sign up for a developer app, and put your key in the database generator \ \ @@ -23,14 +22,15 @@ webbyBits.py .env ``` -1. Place mp3 files in the `sound/` folder +1. Place audio files in the `sound/` folder + - Supports flac, mp3, and wav files 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: `19054` ) + - Change the port of the app (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* + * *The `databaseGenerator.py` will index all audio files, and save the information to `songDatabase.db`* + * *If getting images, this process may take a long time with a large amount of audio files* 4. Run `webbyBits.py` * *The port can be customized by editing the `.env` file* * *You can add an admin password at runtime with* `-a True` *as an atribute* @@ -46,8 +46,8 @@ Read on for specific information on each piece of the app. ## Details These are specific details on each section of the app, and how to use them ### Server: -- `sound/` contains all mp3 files by default -- `databaseGenerator.py` scans through mp3 files and gets information about them +- `sound/` contains all audio files by default +- `databaseGenerator.py` scans through audio files and gets information about them - `Filename, Title, Artist, Art, Length` are all saved - *If the title and artist are not in the file metadata, it looks for a format of* `TITLE_ARTIST.mp3` *then of* `ARTIST - TITLE.mp3` *and otherwise defaults to the file name as the title, and no artist* - Art is retrieved from LastFM From d4a0702a8ae3fe84e0d94f7c65fbd5933472d38d Mon Sep 17 00:00:00 2001 From: Kristy <124598538+kristy-fournier@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:41:25 -0400 Subject: [PATCH 14/19] Add eventlet to requirements this is kind of a half measure, since eventlet is out of date and i have to do monkey_patch all in due time for the stable release will i find a new server to use --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index ac8054e..4863182 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ charset-normalizer==3.4.4 click==8.3.1 colorama==0.4.6 dotenv==0.9.9 +eventlet Flask==3.1.2 flask-cors==6.0.2 Flask-SocketIO==5.6.0 From bec0e301b2d7b237999170f33687f5af0dc17698 Mon Sep 17 00:00:00 2001 From: Kristy F <124598538+kristy-fournier@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:43:27 -0400 Subject: [PATCH 15/19] Add checks for no database, print error --- Server/webbyBits.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Server/webbyBits.py b/Server/webbyBits.py index f8758f5..0d05989 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -42,8 +42,11 @@ fileofDB = sql.connect("songDatabase.db") songDatabase = fileofDB.cursor() #song directory -songDatabase.execute("SELECT * FROM meta WHERE id='songDirectory';") -soundLocation = songDatabase.fetchall()[0][1] +try: + songDatabase.execute("SELECT * FROM meta WHERE id='songDirectory';") + soundLocation = songDatabase.fetchall()[0][1] +except sql.OperationalError: + print("No Database Found, try running databaseGenerator.py") if soundLocation[-1] == "/" or soundLocation[-1] == "\\": pass elif "/" in soundLocation: From 1518fdec8fcf21f542233b053df274fbaaf2e8ae Mon Sep 17 00:00:00 2001 From: Kristy F <124598538+kristy-fournier@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:42:09 -0400 Subject: [PATCH 16/19] Add decription to password option --- Server/webbyBits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/webbyBits.py b/Server/webbyBits.py index 0d05989..2b611af 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -12,7 +12,7 @@ REL_VER_NUM = "0.0.1" # Argparse Stuff parser=argparse.ArgumentParser(description="Options for the Webby Bits") -parser.add_argument('-a','--admin',help="Set as True to be prompted to",default=False) +parser.add_argument('-a','--admin',help="Set as True to be prompted to enter an AdminPassword",default=False) args = parser.parse_args() dotenv.load_dotenv() portTheUserPicked=os.getenv("SERVER_PORT") From 151ed839c3cf1b29fe3ee045b4ed769366580d9e Mon Sep 17 00:00:00 2001 From: Kristy F <124598538+kristy-fournier@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:45:30 -0400 Subject: [PATCH 17/19] updated gitignore for linux --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 87c9ad3..a2d6b61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -server/sound/ +Server/sound/ *.db start.bat .env From 8a5534482be6d379b39a9193ae17832306e2c165 Mon Sep 17 00:00:00 2001 From: Kristy F <124598538+kristy-fournier@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:53:26 -0400 Subject: [PATCH 18/19] Add versionNum Object, pages for search This will be tweaked a lot more but i really need to go study for something instead of doing this --- Server/__pycache__/versionNum.cpython-313.pyc | Bin 0 -> 4355 bytes Server/static/scripts.js | 14 +++- Server/templates/index.html | 2 +- Server/versionNum.py | 67 ++++++++++++++++++ Server/webbyBits.py | 26 ++++--- wishlist.md | 2 +- 6 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 Server/__pycache__/versionNum.cpython-313.pyc create mode 100644 Server/versionNum.py diff --git a/Server/__pycache__/versionNum.cpython-313.pyc b/Server/__pycache__/versionNum.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9f0a4b7c21233a25f04d43394470e70b2bc64a6 GIT binary patch literal 4355 zcmbUkTTmO<^{%ulRzgBD5(Z1em2Ki$I7qR9*4Efe8NUc|X`6+P08KOk)>v3tk-JOA z(0uUW>5xe}hG`j#si(+aF*8j<`V;%hKmD-#V`-EQ%8uLVwEgiPDemOA=j^ToQWlTX zJ8<^gbM8I&p65N#;4-nQ_pxp{)Lmuu;~c1Gr$nCN(3S>Z<9fWGC|TtjuOG* zCxW%tqOYQsxK&`IF+!3b6B4%opA&3CgTVK)#tJrWd8bjw_d>zm3-WPL8?12a_+A)i zt1I}p{V3U;2Z=VfRrto7ubm5yI2*S`2MB2*-Ih9DqsF^s%5%nTaW)DXn(TAMd64-> zQ(lwC>#XNZyQkeD&vR5Q#Hfu)Ny;ioC6$%r$-U}Y^$D{R2V|AR31tLAEdoQWLL*vz zhNWzr6P$t-@m^yuwFxeP)wm5plfY>hFSrGphS`M%KcNnR2iPdssdL&9@~Ey~id2Rh z49#UGMEV?^ImsCU@q+}o53|z)$SR3>(n!@tESNU@Mo8?67%E`0ol%iTwl0tEJ$fnxC`hr4J(zMzj&1J5F&rIEmLNxaVDW<*ZxJI)X zV`E#bqjotMC$a=!k^G}Ma3``7$sa2CPHp7wTA%K@z zCF9iqfMBR*1F(z%RHG7BDEI}cQD^I@P>t)yt~)4lL!4%BGs{AG;lT%x)o)2M-pepB zH<`{#qUx=+tw!|0D)|_IMS^yD;UrVu=>#iuZFpG433F~p!V7%lxJe66UG5aa1I2z)qE9J?=Gk(AccWYW+9Z{ zr>HN*jHh-0Aq#DSk7}o)GDjtdK^a{bH0;)pNI(cf`Dm%NoI!4dr7}D(V7v0c)*QR; zSaU3YkUy0l{$%XlSpMoJv)S=w=NFxu?FC=olD*vMz8$?CEjwFsr*gyVV{2o%tA#_M zqBFGgR@vQF;M;Tq?+qd}j8<y@-Gyo;*W?3zni=<0&J>xDW$1FquCmJfthjH+s#XXv8h~A@eHa#AqBKi4 zjHqNMvzghNfV3qN;(HK}zWR7X>T4iH{u2O5;*AC2AZF1ZfFsdvwB1-grY?#+v(z}cerY{7Z9 zvZ5!?{ytiAyz{Y$siv90PmIC`{A#)+?~0pwRC~+)bcOHYTb8TV=)BY!VsWP zK#fLS!?bQ)!!QIW`V9m?{nl~Sra!5AZ^+Wa$iP04;w^NlMaQ+RIbv zW>6DHB9X~X&7~1{CKB(>CDR7Sok(0uQCUf+BvHyH5)_NC>Xh-v)nrCgu4kw8NcsVu z*n!{;1Q7(?0Mr(I$Z>TdsX*U4F{g+!$T^J*ptT@cd#c|WuZ}K~@%q0eFDxt<*lr@W z<{gWT`_YaSux-R%G&9^7^TNh*hh8{&uKhngC)fVpc7{8tOGG)ST_%~5;HVauQmh%z z!M{Z1lIhv&$py!$@ab@O*Mz7f7x-B4{Q2N$G`P?Z3-;;AXz*Mx1{8iQ*r)RVJEvho zAqSf5+8p#VU^0q7KXeU#j0Z3`^~Z*pr)U+GS%9Bq(6K9ZN~`=i-Rgbx60CzdN#s8R z*tRkZ^EYzzTT2gf@*4tR8-5P5&ZUKt_0ZRR%W^Wu5`TEH|7+I1bmZ2+cRvHmzkt?!c=>X{5%`L2#|2lG`{ + // searchSongs(searchTerm,page+1); + // }) + // x.textContent = "Next Page" + // document.getElementById("songlist-mode").appendChild(x) for(var fileName in searchResults) { let currentSongInJSON = searchResults[fileName] let newItem = document.createElement("div"); @@ -241,6 +247,8 @@ async function searchSongs(searchTerm){ if (JSON.stringify(searchResults)==JSON.stringify({})) { //display error if no results document.getElementById("songlist").innerHTML = "

We might not have that one...

"; + } else { + } } diff --git a/Server/templates/index.html b/Server/templates/index.html index 8775d11..1cbc5cf 100644 --- a/Server/templates/index.html +++ b/Server/templates/index.html @@ -113,7 +113,7 @@ changes visibility with JS-->

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

-

Release {{ REL_VER_NUM }}

+

Release {{ REL_VER_NUM }}

diff --git a/Server/versionNum.py b/Server/versionNum.py new file mode 100644 index 0000000..123cfbd --- /dev/null +++ b/Server/versionNum.py @@ -0,0 +1,67 @@ +from __future__ import annotations +class VersionNumber: + def __init__(self,major:int,minor:int,patch:int,extra:str): + self.major = major + self.minor = minor + self.patch = patch + self.extra = extra + + # From String like "x.y.z-extra" + @staticmethod + def fromString(verString:str) -> VersionNumber: + numList = verString.split(".") + major = int(numList[0]) + minor = int(numList[1]) + patch = int(numList[2].split("-")[0]) + extra = numList[2].split("-")[1] + return VersionNumber(major,minor,patch,extra) + + + def clone(verNumIn:VersionNumber) -> VersionNumber: + return VersionNumber(verNumIn.major,verNumIn.minor,verNumIn.patch,verNumIn.extra) + + def __str__(self) -> str: + returnStr = f"v{self.major}.{self.minor}.{self.patch}" + if(self.extra): + returnStr += f"-{self.extra}" + return returnStr + + def __eq__(self,comp) -> bool: + if type(comp) == VersionNumber: + return self.major == comp.major and self.minor == comp.minor and self.patch == comp.patch and self.extra == comp.extra + elif type(comp) == str: + return self == VersionNumber.fromString(comp) + elif type(comp) == type(None): + return False + else: + raise TypeError + + def __gt__(self,comp): + if type(comp) == VersionNumber: + if(self.major > comp.major): + return True + elif(self.major == comp.major and self.minor > comp.minor): + return True + elif(self.major == comp.major and self.minor == comp.minor and self.patch > comp.patch): + return True + return False + elif type(comp) == str: + return self > VersionNumber.fromString(comp) + else: + raise TypeError + + def __ge__(self,comp): + return self > comp or self == comp + +if __name__ == "__main__": + x = VersionNumber(1,2,4,"alpha") + y = VersionNumber.fromString("1.2.3-beta") + z = VersionNumber.clone(x) + print(x) + print(y) + print(z) + print(f"X == Y: {x==y}") + print(f"X > Y: {x>y}") + print(f"Y < X: {y= Y: {z>=y}") + print(f"Z <= Y: {z<=y}") diff --git a/Server/webbyBits.py b/Server/webbyBits.py index 2b611af..74f98df 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -1,14 +1,14 @@ -import eventlet -eventlet.monkey_patch() +# import eventlet +# eventlet.monkey_patch() from flask import Flask from flask import request,render_template -from flask_cors import CORS from flask_socketio import SocketIO import sqlite3 as sql import vlc,threading,random,argparse,dotenv,os,hashlib,string,getpass +from versionNum import VersionNumber # So i'm famously bad at following Semantic versioning, we're gonna see how this goes -REL_VER_NUM = "0.0.1" +REL_VER_NUM = VersionNumber(0,0,2,"alpha") # Argparse Stuff parser=argparse.ArgumentParser(description="Options for the Webby Bits") @@ -19,7 +19,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 +ERR_MISSING_ARGS = ({"error":"Request missing required arguments","data":None},400) if bool(args.admin) and args.admin.lower() != "false": ADMIN_PASS = hashlib.sha256(bytes(getpass.getpass("Enter AdminPass: "),'utf-8')).hexdigest() else: @@ -47,6 +47,7 @@ try: soundLocation = songDatabase.fetchall()[0][1] except sql.OperationalError: print("No Database Found, try running databaseGenerator.py") + os._exit(1) if soundLocation[-1] == "/" or soundLocation[-1] == "\\": pass elif "/" in soundLocation: @@ -147,7 +148,7 @@ def handleConnect(): @app.route("/",methods=['GET']) def returnStaticFile(): - return render_template("index.html",REL_VER_NUM=REL_VER_NUM) + return render_template("index.html",REL_VER_NUM=str(REL_VER_NUM)) @app.route("/controls", methods=['POST']) def playerControls(): @@ -232,6 +233,9 @@ def settingsControl(): @app.route("/search", methods=['GET']) def searchSongDB(): recieveData = request.args.get("query") + page = int(request.args.get("page")) + if not(page): + page = 1 fileofDB = sql.connect("songDatabase.db") songDatabase = fileofDB.cursor() try: @@ -243,6 +247,12 @@ def searchSongDB(): else: songDatabase.execute("SELECT * FROM virtualSongs WHERE virtualSongs MATCH ?",['"' + recieveData +'"']) results = songDatabase.fetchall() + pages = (len(results)//20)+1 + if(page>0): + # Numbers <0 use old rendering + inBound = 20*(page-1) + outBound = 20*page + results = results[inBound:outBound] tempdata = {} # this is a temporary solution so i dont have to change the client for i in results: @@ -255,7 +265,7 @@ def searchSongDB(): } fileofDB.close() - return {"error":"ok","data":tempdata},200 + return {"error":"ok","data":{"songsobj":tempdata,"pages":pages}},200 except sql.OperationalError as e: print(e) fileofDB.close() @@ -325,6 +335,6 @@ if __name__ == "__main__": queueThread = threading.Thread(target=playQueuedSongs) queueThread.daemon = True queueThread.start() - print(f"PartyJukebox v{REL_VER_NUM} running on port {portTheUserPicked}") + print(f"PartyJukebox {REL_VER_NUM} running on port {portTheUserPicked}") socketio.run(app=app,host='0.0.0.0', port=portTheUserPicked) \ No newline at end of file diff --git a/wishlist.md b/wishlist.md index 2602f75..13d6bfb 100644 --- a/wishlist.md +++ b/wishlist.md @@ -14,7 +14,7 @@ - [ ] Security Updates - [x] `.env` file for the api keys and other runtime info to be set, rather than in the `.py` files - [x] Hashing rather than plaintext sending passwords (that way at least the password text itself isn't transmitted over the network) - - [ ] Actually use TLS, for posting (CORS seems like an issue) + - [ ] Actually use TLS, for posting - [ ] Accessibility - [ ] Better use of semantic HTML tags - [ ] Full keyboard control (tab, enter to select, tab between control buttons) From 9c7c33ffc12747bffdfbaaa4e57695982d5cd3a6 Mon Sep 17 00:00:00 2001 From: Kristy F <124598538+kristy-fournier@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:45:52 -0400 Subject: [PATCH 19/19] Added minor TODO --- Server/static/scripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/static/scripts.js b/Server/static/scripts.js index 688c8a8..1cd85e7 100644 --- a/Server/static/scripts.js +++ b/Server/static/scripts.js @@ -626,7 +626,7 @@ document.getElementById("playlist-mode").style.display = "none"; document.getElementById("settings-mode").style.display = "none"; document.getElementById("volumerange").onchange = async function(e) { // there is no reason for this not to be a defined function - // FIX THIS + // TODO: FIX THIS let returnValue = await postFromServer({setting:"volume",level:e.target.value}, "settings") if (returnValue["status"] == ERR_NO_ADMIN) { // alertText("Error: Admin restricted action");