From a78b15aab70edaeb0d5b02022a437a390155edf7 Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Sun, 6 Jul 2025 18:58:37 -0400
Subject: [PATCH 01/70] Start of docker stuff
---
.env | 5 +++++
Client/Dockerfile | 7 +++++++
Compose.yaml | 19 +++++++++++++++++++
Server/Dockerfile | 14 ++++++++++++++
Server/databaseGenerator.py | 16 +++++++++-------
requirements.txt => Server/requirements.txt | 0
Server/webbyBits.py | 6 ++++--
7 files changed, 58 insertions(+), 9 deletions(-)
create mode 100644 .env
create mode 100644 Client/Dockerfile
create mode 100644 Compose.yaml
create mode 100644 Server/Dockerfile
rename requirements.txt => Server/requirements.txt (100%)
diff --git a/.env b/.env
new file mode 100644
index 0000000..afc0cf5
--- /dev/null
+++ b/.env
@@ -0,0 +1,5 @@
+CLIENT_PORT = 8000
+SERVER_PORT = 19054
+
+SOUND_LOCATION = C:\Users\dylan\Music
+DB_LOCATION = C:\Users\dylan\Document
\ No newline at end of file
diff --git a/Client/Dockerfile b/Client/Dockerfile
new file mode 100644
index 0000000..82c49c9
--- /dev/null
+++ b/Client/Dockerfile
@@ -0,0 +1,7 @@
+FROM httpd:2.4
+WORKDIR /app
+
+COPY . /usr/local/apache2/htdocs
+
+EXPOSE 80
+
diff --git a/Compose.yaml b/Compose.yaml
new file mode 100644
index 0000000..29a2b7e
--- /dev/null
+++ b/Compose.yaml
@@ -0,0 +1,19 @@
+name: party-jukebox
+
+services:
+ pj-client:
+ build: ./client
+ ports:
+ - ${CLIENT_PORT:-80}:80
+ pj-db-builder:
+ # tbd
+ pj-server:
+ build: ./server
+ depends_on:
+ pj-db-builder:
+ condition: service_completed_successfully
+ ports:
+ - ${SERVER_PORT:-19054}:19054
+ volumes:
+ - ${SOUND_LOCATION}:/sound
+ - ${DB_LOCATION}:/db
\ No newline at end of file
diff --git a/Server/Dockerfile b/Server/Dockerfile
new file mode 100644
index 0000000..047c96f
--- /dev/null
+++ b/Server/Dockerfile
@@ -0,0 +1,14 @@
+FROM python:3.13
+workdir /app2
+
+COPY . .
+
+RUN apt-get update
+RUN apt-get upgrade
+#RUN apt-get install vlc -y
+#RUN pip install -r requirements.txt
+
+# RUN python3 databaseGenerator.py -m new -d /sound
+
+#CMD ["python3","webbyBits.py"]
+RUN ls
\ No newline at end of file
diff --git a/Server/databaseGenerator.py b/Server/databaseGenerator.py
index e83cbc3..65c18ee 100644
--- a/Server/databaseGenerator.py
+++ b/Server/databaseGenerator.py
@@ -11,19 +11,21 @@ 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('-b','--database',help="Location of the .db file",default='.')
args = parser.parse_args()
apikeylastfm = args.apikey
-if args.directory[-1] == "/" or args.directory[-1] == "\\":
- soundLocation = args.directory
-elif "/" in args.directory:
- soundLocation = args.directory + "/"
-else:
- soundLocation = args.directory + "\\"
+soundLocation = args.directory
+# if args.directory[-1] == "/" or args.directory[-1] == "\\":
+# soundLocation = args.directory
+# elif "/" in args.directory:
+# soundLocation = args.directory + "/"
+# else:
+# soundLocation = args.directory + "\\"
# if you want to set the api key/sound directory permenantly for your setup just uncomment the next line
# apikeylastfm = "KeyHere"
# soundLocation = "directoryHere"
songFiles = os.listdir(soundLocation)
-fileOfDB = sql.connect("songDatabase.db")
+fileOfDB = sql.connect(args.database+"/songDatabase.db")
songDatabase = fileOfDB.cursor()
# setting song directory
songDatabase.execute("CREATE TABLE IF NOT EXISTS meta (id TEXT PRIMARY KEY, data TEXT);")
diff --git a/requirements.txt b/Server/requirements.txt
similarity index 100%
rename from requirements.txt
rename to Server/requirements.txt
diff --git a/Server/webbyBits.py b/Server/webbyBits.py
index 614e1b4..99f4a5e 100644
--- a/Server/webbyBits.py
+++ b/Server/webbyBits.py
@@ -8,9 +8,11 @@ parser=argparse.ArgumentParser(description="Options for the Webby Bits")
# this is no longer needed assuming my file works correctly with the generator
# parser.add_argument('-d','--directory',help="Directory of the song files (make sure this matches the directory used for the databaseGenerator)", default="./sound/")
parser.add_argument('-p','--port',help="Port to host on, not the same as the web (client) port",default='19054')
-portTheUserPicked=parser.parse_args().port
+parser.add_argument('-b','--database',help="Location of the .db file",default='.')
+args = parser.parse_args()
+portTheUserPicked=args.port
-fileofDB = sql.connect("songDatabase.db")
+fileofDB = sql.connect(args.database+"/songDatabase.db")
songDatabase = fileofDB.cursor()
#song directory
From a291e4626aef4588325ac476e5d8f99718b09108 Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Sat, 12 Jul 2025 22:09:18 -0400
Subject: [PATCH 02/70] added options
---
Client/index.html | 21 +++++++++++++++++++--
Client/scripts.js | 8 ++++++++
2 files changed, 27 insertions(+), 2 deletions(-)
diff --git a/Client/index.html b/Client/index.html
index 6012ac4..23420b7 100644
--- a/Client/index.html
+++ b/Client/index.html
@@ -81,13 +81,30 @@ changes visibility with JS-->
Volume of the music
-
+
Share the remote:
-
Version 1.0.2
+
Admin Settings
+
Note: Admin password must have been set from the server
+
+
Admin Password:
+
Enter to use admin restricted functions
+
+
+
+
Fine action control:
+
A check means that action is avalible to everyone
+
+
+
+
+
+
+
+
PartyJukebox is under an AGPLV3 liscense. You can access the source code here.
diff --git a/Client/scripts.js b/Client/scripts.js
index a63ee01..10bbd5f 100644
--- a/Client/scripts.js
+++ b/Client/scripts.js
@@ -192,6 +192,14 @@ async function checkSettings(skipServer=false) {
qrCodeGenerate()
document.getElementById("alerttimetextbox").value = alertTime
partyButtonState = document.getElementById("partymode-button").innerHTML;
+ checksforadmin = document.getElementById("admincheckholder")
+ // temporary
+ for (let i in checksforadmin.children) {
+ if (i.type == "checkbox") {
+ i.checked = true;
+ }
+ }
+ //ping the server here
x = await getFromServer({setting: "getsettings"}, "settings");
if (!(skipServer) || partyButtonState=="N/A") {
if (x["partymode"] == false) {
From 26e8e937852a72198c10c11773a0e616e4db5018 Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Fri, 18 Jul 2025 18:31:02 -0400
Subject: [PATCH 03/70] Fixed adminperms labels
---
Client/index.html | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/Client/index.html b/Client/index.html
index 23420b7..7d14a89 100644
--- a/Client/index.html
+++ b/Client/index.html
@@ -98,11 +98,11 @@ changes visibility with JS-->
Fine action control:
A check means that action is avalible to everyone
-
-
-
-
-
+
+
+
+
+
PartyJukebox is under an AGPLV3 liscense. You can access the source code here.
From ea0380e3eb4c16c9dd00213f3c46688f1eb737d4 Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Fri, 18 Jul 2025 18:31:22 -0400
Subject: [PATCH 04/70] changed a for loop to fix for testing
---
Client/scripts.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/Client/scripts.js b/Client/scripts.js
index 10bbd5f..7ee24de 100644
--- a/Client/scripts.js
+++ b/Client/scripts.js
@@ -192,11 +192,11 @@ async function checkSettings(skipServer=false) {
qrCodeGenerate()
document.getElementById("alerttimetextbox").value = alertTime
partyButtonState = document.getElementById("partymode-button").innerHTML;
- checksforadmin = document.getElementById("admincheckholder")
+ let nodeList = document.getElementById("admincheckholder").children
// temporary
- for (let i in checksforadmin.children) {
- if (i.type == "checkbox") {
- i.checked = true;
+ for (let i=0; i
Date: Fri, 18 Jul 2025 18:31:46 -0400
Subject: [PATCH 05/70] Added base of password system and check for adding a
song
---
Server/webbyBits.py | 37 +++++++++++++++++++++++++++++++++----
1 file changed, 33 insertions(+), 4 deletions(-)
diff --git a/Server/webbyBits.py b/Server/webbyBits.py
index 614e1b4..d8fe771 100644
--- a/Server/webbyBits.py
+++ b/Server/webbyBits.py
@@ -8,7 +8,20 @@ parser=argparse.ArgumentParser(description="Options for the Webby Bits")
# this is no longer needed assuming my file works correctly with the generator
# parser.add_argument('-d','--directory',help="Directory of the song files (make sure this matches the directory used for the databaseGenerator)", default="./sound/")
parser.add_argument('-p','--port',help="Port to host on, not the same as the web (client) port",default='19054')
-portTheUserPicked=parser.parse_args().port
+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="")
+args = parser.parse_args()
+portTheUserPicked=args.port
+ADMIN_PASS = args.admin
+if not(ADMIN_PASS):
+ ADMIN_PASS = None
+# True = everyone, False = admin only. Change in client while in use.
+controlPerms = {
+ "PP":True,
+ "SK":True,
+ "AS":True,
+ "PM":True,
+ "VOL":True
+}
fileofDB = sql.connect("songDatabase.db")
songDatabase = fileofDB.cursor()
@@ -119,9 +132,16 @@ def settingsControl():
elif recieveData["setting"] == "partymode-toggle":
partyMode = not(partyMode)
return "200"
+ elif recieveData["setting"] == "perms":
+ if ADMIN_PASS == recieveData["password"] and ADMIN_PASS:
+ controlPerms = recieveData["admin"]
+ return "200"
+ else:
+ return "401"
+
elif recieveData["setting"] == "getsettings":
# probably should have made this a different request type or something but it works
- x = {"partymode":partyMode,"volume":player.audio_get_volume()}
+ x = {"partymode":partyMode,"volume":player.audio_get_volume(),"admin":controlPerms}
return x
else:
return "400"
@@ -154,8 +174,17 @@ def searchSongDB():
@app.route("/songadd", methods=["POST"])
def songadd():
recieveData=request.get_json(force=True)
- queueSong(recieveData['song'])
- return "200"
+ if ADMIN_PASS and ADMIN_PASS == recieveData['password']:
+ # Pass exists and is correct
+ queueSong(recieveData['song'])
+ return "200"
+ elif ADMIN_PASS and not(controlPerms["AS"]):
+ # Pass exists, or this action isn't restricted
+ return "401"
+ else:
+ # No pass
+ queueSong(recieveData['song'])
+ return "200"
@app.route("/playlist", methods=["POST"])
def getPlaylist():
global songNext
From f41255e45653934e43a3017185d0da9983d3c0a0 Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Fri, 18 Jul 2025 19:21:21 -0400
Subject: [PATCH 06/70] added server side password verification to everything
that needs it
---
Server/webbyBits.py | 44 +++++++++++++++++++++++++++++---------------
1 file changed, 29 insertions(+), 15 deletions(-)
diff --git a/Server/webbyBits.py b/Server/webbyBits.py
index d8fe771..22a84c5 100644
--- a/Server/webbyBits.py
+++ b/Server/webbyBits.py
@@ -16,11 +16,11 @@ if not(ADMIN_PASS):
ADMIN_PASS = None
# True = everyone, False = admin only. Change in client while in use.
controlPerms = {
- "PP":True,
- "SK":True,
- "AS":True,
- "PM":True,
- "VOL":True
+ "PP":True, #done
+ "SK":True, #done
+ "AS":True, #done
+ "PM":True, #done
+ "VOL":True #done
}
fileofDB = sql.connect("songDatabase.db")
@@ -111,34 +111,48 @@ def playerControls():
recieveData=request.get_json(force=True)
if recieveData["control"] != None:
if recieveData["control"] == "play-pause":
- player.pause()
- return "200"
+ if ADMIN_PASS == recieveData['password'] or not(ADMIN_PASS) or controlPerms["PP"]:
+ player.pause()
+ return "200"
+ else:
+ return "401"
elif recieveData["control"] == "skip":
- skipNow = True
- # print(str(player.get_state()))
- return "200"
+ if ADMIN_PASS == recieveData['password'] or not(ADMIN_PASS) or controlPerms["SK"]:
+ skipNow = True
+ return "200"
+ else:
+ return "401"
else:
return "400"
@app.route("/settings", methods=['POST'])
def settingsControl():
+ global controlPerms
# set the volume and partymode
global partyMode
global player
recieveData = request.get_json(force=True)
if recieveData["setting"] == "volume":
- volumePassed = player.audio_set_volume(int(recieveData["level"]))
- return {"volumePassed":volumePassed}
+ if ADMIN_PASS == recieveData['password'] or not(ADMIN_PASS) or controlPerms["VOL"]:
+ volumePassed = player.audio_set_volume(int(recieveData["level"]))
+ return {"volumePassed":volumePassed}
+ else:
+ return "401"
elif recieveData["setting"] == "partymode-toggle":
- partyMode = not(partyMode)
- return "200"
+ if ADMIN_PASS == recieveData['password'] or not(ADMIN_PASS) or controlPerms["PM"]:
+ partyMode = not(partyMode)
+ return "200"
+ else:
+ return "401"
elif recieveData["setting"] == "perms":
+ print(ADMIN_PASS)
+ print(recieveData["password"])
if ADMIN_PASS == recieveData["password"] and ADMIN_PASS:
+ #if an adminpass doesn't exist these perms can never be changed
controlPerms = recieveData["admin"]
return "200"
else:
return "401"
-
elif recieveData["setting"] == "getsettings":
# probably should have made this a different request type or something but it works
x = {"partymode":partyMode,"volume":player.audio_get_volume(),"admin":controlPerms}
From 1910b30acc32ea6979ed8a171b4d01df8e73e1fe Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Fri, 18 Jul 2025 20:13:18 -0400
Subject: [PATCH 07/70] everything but the auto-updating settings should be
working
and also skipping isnt working on the playlist page
---
Client/index.html | 4 ++--
Client/scripts.js | 33 ++++++++++++++++++++++++++++++---
2 files changed, 32 insertions(+), 5 deletions(-)
diff --git a/Client/index.html b/Client/index.html
index 7d14a89..8bd1d0a 100644
--- a/Client/index.html
+++ b/Client/index.html
@@ -92,7 +92,7 @@ changes visibility with JS-->
Admin Password:
Enter to use admin restricted functions
-
+
Fine action control:
@@ -101,7 +101,7 @@ changes visibility with JS-->
-
+
diff --git a/Client/scripts.js b/Client/scripts.js
index 7ee24de..07d5d73 100644
--- a/Client/scripts.js
+++ b/Client/scripts.js
@@ -1,5 +1,6 @@
-let ip
-let alertTime = 2
+let ip;
+let alertTime = 2;
+let adminPass = "";
async function alertText(text="Song Added!") {
alertbox = document.getElementById("alert");
alertbox.innerHTML = text;
@@ -10,8 +11,11 @@ async function alertText(text="Song Added!") {
}
// 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="") {
+async function getFromServer(bodyInfo, source="",password=adminPass) {
try{
+ if (bodyInfo != null) {
+ bodyInfo["password"] = password;
+ }
const response = await fetch("http://"+ip+"/"+source, {
method: "POST",
body: JSON.stringify(bodyInfo),
@@ -20,10 +24,15 @@ async function getFromServer(bodyInfo, source="") {
}
});
const data = await response.json();
+ if (data == "401") {
+ alertText("error: Admin restricted action")
+ }
return await data;
} catch(e) {
if (e == "TypeError: Failed to fetch"){
alertText("error: Can't Connect to Server (is the ip set?)")
+ } else if(e == "") {
+
} else {
alertText("error: " + e)
}
@@ -309,6 +318,22 @@ function toggleDark(e) {
}
}
+function adminPassEnter(e) {
+ if (e.key == "Enter") {
+ e.preventDefault();
+ adminPass=document.getElementById("adminpasswordbox").value
+ alertText("Admin Password Updated")
+ }
+}
+function submitPerms() {
+ let tempData = {}
+ tempData["PP"] = document.getElementById("playpausesettingcheckbox").checked
+ tempData["SK"] = document.getElementById("skipsongsettingcheckbox").checked
+ tempData["AS"] = document.getElementById("addsongsettingcheckbox").checked
+ tempData["PM"] = document.getElementById("partymodesettingcheckbox").checked
+ tempData["VOL"] = document.getElementById("partymodesettingcheckbox").checked
+ getFromServer({"setting":"perms","admin":tempData},"settings")
+}
let optionslist = []
@@ -344,6 +369,8 @@ document.getElementById("go-search").addEventListener('click', function(){search
document.getElementById("songsearch").addEventListener('keydown', function(e){searchSongsEnter(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(){submitPerms()});
document.getElementById("partymode-button").addEventListener('click',function(){controlButton("pm")})
//sets the fact that clicking a song needs to return its id to the function to find it
document.getElementById("songlist").addEventListener('click', function(e){checkWhatSongWasClicked(e)});
From 36c2286b41a12f0735554e419a9a5cc9a3809ccc Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Fri, 3 Oct 2025 15:32:14 -0400
Subject: [PATCH 08/70] Update readme.md
---
readme.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/readme.md b/readme.md
index e63bcbd..59f555b 100644
--- a/readme.md
+++ b/readme.md
@@ -65,6 +65,7 @@ From left to right:
- The currently playing song is identified, and has the duration listed
- The play-pause button toggles playing
- The skip button goes to the next track
+ - *No "previous" button is a design decision (It's a feature not a bug)*
- The search button opens the search screen (pictured)
- The settings button (top right) opens the settings menu
- Server IP allows you to change the ip that the site connects to
From 1733a485b4ab874750e219f4db9626c073adc922 Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Fri, 3 Oct 2025 17:26:12 -0400
Subject: [PATCH 09/70] 99% done
also added some new comments and notes about future design changes
---
Client/scripts.js | 33 ++++++++++++++++++++++++++-------
Server/webbyBits.py | 25 ++++++++++++++-----------
2 files changed, 40 insertions(+), 18 deletions(-)
diff --git a/Client/scripts.js b/Client/scripts.js
index 07d5d73..2f8130d 100644
--- a/Client/scripts.js
+++ b/Client/scripts.js
@@ -222,6 +222,16 @@ async function checkSettings(skipServer=false) {
document.getElementById("partymode-button").innerHTML = "Off";
}
document.getElementById("volumerange").value = parseInt(x["volume"])
+
+ // do the admin checkboxes here
+ // seemingly i almost finished it last time, dunno why i stopped here
+ // like as far as i can tell this is the last step
+ let currentAdminPerms = x["admin"];
+ document.getElementById("addsongsettingcheckbox").checked = currentAdminPerms["AS"];
+ document.getElementById("skipsongsettingcheckbox").checked = currentAdminPerms["SK"];
+ document.getElementById("playpausesettingcheckbox").checked = currentAdminPerms["PP"];
+ document.getElementById("partymodesettingcheckbox").checked = currentAdminPerms["PM"];
+ document.getElementById("volumechangesettingcheckbox").checked = currentAdminPerms["VOL"];
}
async function generateVisualPlaylist(conditions="") {
@@ -325,14 +335,20 @@ function adminPassEnter(e) {
alertText("Admin Password Updated")
}
}
-function submitPerms() {
+async function submitPerms() {
let tempData = {}
- tempData["PP"] = document.getElementById("playpausesettingcheckbox").checked
- tempData["SK"] = document.getElementById("skipsongsettingcheckbox").checked
- tempData["AS"] = document.getElementById("addsongsettingcheckbox").checked
- tempData["PM"] = document.getElementById("partymodesettingcheckbox").checked
- tempData["VOL"] = document.getElementById("partymodesettingcheckbox").checked
- getFromServer({"setting":"perms","admin":tempData},"settings")
+ tempData["PP"] = document.getElementById("playpausesettingcheckbox").checked;
+ tempData["SK"] = document.getElementById("skipsongsettingcheckbox").checked;
+ tempData["AS"] = document.getElementById("addsongsettingcheckbox").checked;
+ tempData["PM"] = document.getElementById("partymodesettingcheckbox").checked;
+ tempData["VOL"] = document.getElementById("partymodesettingcheckbox").checked;
+ let returncode = await getFromServer({"setting":"perms","admin":tempData},"settings");
+ if (returncode === "401") {
+ // just so that the checkboxes don't change if you click without the
+ // (I know i could do this better but this is good enough for now)
+ // okay this actually doesn't work but i have to go eat dinner
+ checkSettings();
+ }
}
let optionslist = []
@@ -350,6 +366,8 @@ document.getElementById("settings-mode").style.display = "none";
document.getElementById("volumerange").onchange = async function() {
let returnValue = await getFromServer({setting:"volume",level:this.value}, "settings")
if (returnValue["volumePassed"] !=0) {
+ // i forgot about this, i had to do this because it confused the crap out of me one time
+ // vlc doesn't let you change the volume of nothing, which makes sense if you think about it
alertText("Nothing is playing")
document.getElementById("volumerange").value = -1
}
@@ -378,6 +396,7 @@ document.getElementById("songlist").addEventListener('click', function(e){checkW
let tempWidth = document.getElementById('controls').clientWidth;
document.getElementById("controls").style.marginLeft = "-"+String(parseInt(tempWidth/2))+"px";
// document.getElementById("darkmode-button").addEventListener('click',function(){toggleDark()})
+
//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)
diff --git a/Server/webbyBits.py b/Server/webbyBits.py
index 22a84c5..fb81d9f 100644
--- a/Server/webbyBits.py
+++ b/Server/webbyBits.py
@@ -10,7 +10,11 @@ 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="")
args = parser.parse_args()
+
+
portTheUserPicked=args.port
+#Just a note that the return code "401" as of now is used to mean "you don't have the password"
+#This is not great design, and the whole "returning string codes" thing is something to add to the todo list
ADMIN_PASS = args.admin
if not(ADMIN_PASS):
ADMIN_PASS = None
@@ -36,7 +40,7 @@ elif "/" in soundLocation:
soundLocation += "/"
else:
soundLocation += "\\"
-print(soundLocation)
+#print(soundLocation)
#Create Virtual table for searching
songDatabase.execute("DROP TABLE virtualSongs;")
songDatabase.execute("CREATE VIRTUAL TABLE virtualSongs USING fts5(filename, title, artist, art, length);")
@@ -58,7 +62,7 @@ player = fakeplayer.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
+# because you are POSTing from another domain to this one, you need CORS
CORS(app)
def queueSong(song):
@@ -124,6 +128,8 @@ def playerControls():
return "401"
else:
return "400"
+ else:
+ return "400"
@app.route("/settings", methods=['POST'])
def settingsControl():
@@ -145,8 +151,8 @@ def settingsControl():
else:
return "401"
elif recieveData["setting"] == "perms":
- print(ADMIN_PASS)
- print(recieveData["password"])
+ # print(ADMIN_PASS)
+ # print(recieveData["password"])
if ADMIN_PASS == recieveData["password"] and ADMIN_PASS:
#if an adminpass doesn't exist these perms can never be changed
controlPerms = recieveData["admin"]
@@ -188,17 +194,14 @@ def searchSongDB():
@app.route("/songadd", methods=["POST"])
def songadd():
recieveData=request.get_json(force=True)
- if ADMIN_PASS and ADMIN_PASS == recieveData['password']:
- # Pass exists and is correct
+ if (ADMIN_PASS and ADMIN_PASS == recieveData['password']):
+ # Pass exists and is correct, or it's not restricted
queueSong(recieveData['song'])
return "200"
- elif ADMIN_PASS and not(controlPerms["AS"]):
+ else:
# Pass exists, or this action isn't restricted
return "401"
- else:
- # No pass
- queueSong(recieveData['song'])
- return "200"
+
@app.route("/playlist", methods=["POST"])
def getPlaylist():
global songNext
From 76971ea75e6b8e76b54594f77f387da383ce5e62 Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Fri, 3 Oct 2025 18:51:12 -0400
Subject: [PATCH 10/70] Done adding admin password
Just gotta do documentation
---
Client/scripts.js | 30 +++++++++++++++++-------------
Client/styles.css | 8 ++++++++
Server/webbyBits.py | 18 ++++++++++--------
3 files changed, 35 insertions(+), 21 deletions(-)
diff --git a/Client/scripts.js b/Client/scripts.js
index 2f8130d..02ac217 100644
--- a/Client/scripts.js
+++ b/Client/scripts.js
@@ -1,6 +1,8 @@
let ip;
let alertTime = 2;
let adminPass = "";
+const ERR_NO_ADMIN = "401"; // gonna use this later to refactor
+
async function alertText(text="Song Added!") {
alertbox = document.getElementById("alert");
alertbox.innerHTML = text;
@@ -25,12 +27,12 @@ async function getFromServer(bodyInfo, source="",password=adminPass) {
});
const data = await response.json();
if (data == "401") {
- alertText("error: Admin restricted action")
+ alertText("Error: Admin restricted action")
}
return await data;
} catch(e) {
if (e == "TypeError: Failed to fetch"){
- alertText("error: Can't Connect to Server (is the ip set?)")
+ alertText("Error: Can't Connect to Server (is the ip set?)")
} else if(e == "") {
} else {
@@ -224,8 +226,6 @@ async function checkSettings(skipServer=false) {
document.getElementById("volumerange").value = parseInt(x["volume"])
// do the admin checkboxes here
- // seemingly i almost finished it last time, dunno why i stopped here
- // like as far as i can tell this is the last step
let currentAdminPerms = x["admin"];
document.getElementById("addsongsettingcheckbox").checked = currentAdminPerms["AS"];
document.getElementById("skipsongsettingcheckbox").checked = currentAdminPerms["SK"];
@@ -302,8 +302,12 @@ async function generateVisualPlaylist(conditions="") {
}
async function submitSong(songid) {
- getFromServer({song: songid}, "songadd")
- alertText("Added to Queue")
+ let returncode = await getFromServer({song: songid}, "songadd");
+ if(returncode == ERR_NO_ADMIN) {
+ // right now the error is alerted in getFromServer, maybe will change that
+ } else {
+ alertText("Added to Queue");
+ }
}
function checkWhatSongWasClicked(e) {
itemId = e.srcElement.id;
@@ -335,7 +339,7 @@ function adminPassEnter(e) {
alertText("Admin Password Updated")
}
}
-async function submitPerms() {
+async function submitPerms(e) {
let tempData = {}
tempData["PP"] = document.getElementById("playpausesettingcheckbox").checked;
tempData["SK"] = document.getElementById("skipsongsettingcheckbox").checked;
@@ -343,11 +347,11 @@ async function submitPerms() {
tempData["PM"] = document.getElementById("partymodesettingcheckbox").checked;
tempData["VOL"] = document.getElementById("partymodesettingcheckbox").checked;
let returncode = await getFromServer({"setting":"perms","admin":tempData},"settings");
- if (returncode === "401") {
- // just so that the checkboxes don't change if you click without the
- // (I know i could do this better but this is good enough for now)
- // okay this actually doesn't work but i have to go eat dinner
- checkSettings();
+ if (returncode == ERR_NO_ADMIN) {
+ // 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
+ let clickedBox = e.srcElement;
+ clickedBox.checked = !clickedBox.checked;
}
}
@@ -388,7 +392,7 @@ document.getElementById("songsearch").addEventListener('keydown', function(e){se
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(){submitPerms()});
+document.getElementById("admincheckholder").addEventListener('click',function(e){submitPerms(e)});
document.getElementById("partymode-button").addEventListener('click',function(){controlButton("pm")})
//sets the fact that clicking a song needs to return its id to the function to find it
document.getElementById("songlist").addEventListener('click', function(e){checkWhatSongWasClicked(e)});
diff --git a/Client/styles.css b/Client/styles.css
index 803c5df..a6c4cdf 100644
--- a/Client/styles.css
+++ b/Client/styles.css
@@ -177,6 +177,14 @@ h4 {
border-bottom: 0;
}
+.settings > .item > h2 {
+ margin-bottom: 4px;
+}
+
+.settings > .item > p {
+ margin-top: 0px
+}
+
.versionNumber {
font-size: 8px;
font-style: italic;
diff --git a/Server/webbyBits.py b/Server/webbyBits.py
index fb81d9f..c10a04b 100644
--- a/Server/webbyBits.py
+++ b/Server/webbyBits.py
@@ -13,8 +13,10 @@ args = parser.parse_args()
portTheUserPicked=args.port
-#Just a note that the return code "401" as of now is used to mean "you don't have the password"
-#This is not great design, and the whole "returning string codes" thing is something to add to the todo list
+# Just a note that the return code "401" as of now is used to mean "you don't have the password"
+# This is not great design, and the whole "returning string codes" thing is something to add to the todo list
+# I mean returning 200 when no return is necesary i think is fine but we'll see
+ERR_NO_ADMIN = "401"
ADMIN_PASS = args.admin
if not(ADMIN_PASS):
ADMIN_PASS = None
@@ -119,13 +121,13 @@ def playerControls():
player.pause()
return "200"
else:
- return "401"
+ return ERR_NO_ADMIN
elif recieveData["control"] == "skip":
if ADMIN_PASS == recieveData['password'] or not(ADMIN_PASS) or controlPerms["SK"]:
skipNow = True
return "200"
else:
- return "401"
+ return ERR_NO_ADMIN
else:
return "400"
else:
@@ -143,13 +145,13 @@ def settingsControl():
volumePassed = player.audio_set_volume(int(recieveData["level"]))
return {"volumePassed":volumePassed}
else:
- return "401"
+ return ERR_NO_ADMIN
elif recieveData["setting"] == "partymode-toggle":
if ADMIN_PASS == recieveData['password'] or not(ADMIN_PASS) or controlPerms["PM"]:
partyMode = not(partyMode)
return "200"
else:
- return "401"
+ return ERR_NO_ADMIN
elif recieveData["setting"] == "perms":
# print(ADMIN_PASS)
# print(recieveData["password"])
@@ -158,7 +160,7 @@ def settingsControl():
controlPerms = recieveData["admin"]
return "200"
else:
- return "401"
+ return ERR_NO_ADMIN
elif recieveData["setting"] == "getsettings":
# probably should have made this a different request type or something but it works
x = {"partymode":partyMode,"volume":player.audio_get_volume(),"admin":controlPerms}
@@ -200,7 +202,7 @@ def songadd():
return "200"
else:
# Pass exists, or this action isn't restricted
- return "401"
+ return ERR_NO_ADMIN
@app.route("/playlist", methods=["POST"])
def getPlaylist():
From 189cafd08a30d83fe429f78d5b993117051bcb49 Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Fri, 3 Oct 2025 19:03:49 -0400
Subject: [PATCH 11/70] Readme update
Should be good to be pushed to main now
---
Client/scripts.js | 2 +-
readme.md | 10 ++++++++++
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/Client/scripts.js b/Client/scripts.js
index 02ac217..46d1a54 100644
--- a/Client/scripts.js
+++ b/Client/scripts.js
@@ -347,7 +347,7 @@ async function submitPerms(e) {
tempData["PM"] = document.getElementById("partymodesettingcheckbox").checked;
tempData["VOL"] = document.getElementById("partymodesettingcheckbox").checked;
let returncode = await getFromServer({"setting":"perms","admin":tempData},"settings");
- if (returncode == ERR_NO_ADMIN) {
+ if (returncode == ERR_NO_ADMIN || returncode == null) {
// 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
let clickedBox = e.srcElement;
diff --git a/readme.md b/readme.md
index e63bcbd..0fa7206 100644
--- a/readme.md
+++ b/readme.md
@@ -28,6 +28,7 @@ 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 at runtime using* `-p portNumber` *as an atribute*
+ * *You can add an admin password at runtime with* `-a AdminPass` *as an atribute*
You can now connect with the client and use the app as normal. \
*Make sure you have turned down/off any other apps that might make noise or notification sounds* \
@@ -57,6 +58,15 @@ These are specific details on each section of the app, and how to use them
- Uses port 19054 by default
- `--port (port)` changes the port for that run
- The default port can be changed in the file
+ - Running with `--admin (admin password)` sets an admin password for moderation on the client
+ - Anyone who knows the admin password can enter it on the client and change the abilities of any non-admin users (for example to limit skipping)
+ - The total set of features that can be restricted is
+ - Skip track
+ - Play-pause toggle
+ - Add track
+ - Partymode toggle
+ - Change volume
+ - When this argument is left out (or empty string) the admin features aren't used, and everyone can do everything
### Client:
 \
From d24aca7f3948f624d01dd1bf8daa685bea56ff3a Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Sun, 5 Oct 2025 09:02:48 -0400
Subject: [PATCH 12/70] made very clear passwords are not secure
---
readme.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/readme.md b/readme.md
index 0fa7206..9a79055 100644
--- a/readme.md
+++ b/readme.md
@@ -29,6 +29,7 @@ webbyBits.py
4. Run `webbyBits.py`
* *The port can be customized at runtime using* `-p portNumber` *as an atribute*
* *You can add an admin password at runtime with* `-a AdminPass` *as an atribute*
+ * ***NOTE: Do not reuse ANY password for this, it is 100% unsecure. The best option is just a random string you write down once***
You can now connect with the client and use the app as normal. \
*Make sure you have turned down/off any other apps that might make noise or notification sounds* \
@@ -59,6 +60,7 @@ These are specific details on each section of the app, and how to use them
- `--port (port)` changes the port for that run
- The default port can be changed in the file
- Running with `--admin (admin password)` sets an admin password for moderation on the client
+ - ***Note: Do not reuse a password, consider this like making whatever this string is public, no security is guaranteed***
- Anyone who knows the admin password can enter it on the client and change the abilities of any non-admin users (for example to limit skipping)
- The total set of features that can be restricted is
- Skip track
From 86fde793056e12928479aa7186870c85a8eba337 Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Sun, 5 Oct 2025 09:04:24 -0400
Subject: [PATCH 13/70] make VERY CLEAR the password feature is not secure
---
readme.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/readme.md b/readme.md
index 9a79055..a54546e 100644
--- a/readme.md
+++ b/readme.md
@@ -30,6 +30,7 @@ webbyBits.py
* *The port can be customized at runtime using* `-p portNumber` *as an atribute*
* *You can add an admin password at runtime with* `-a AdminPass` *as an atribute*
* ***NOTE: Do not reuse ANY password for this, it is 100% unsecure. The best option is just a random string you write down once***
+ * This is intended for protecting certain features for small closed events, not for public security
You can now connect with the client and use the app as normal. \
*Make sure you have turned down/off any other apps that might make noise or notification sounds* \
From d72320aae48ba31f0849fef2b1b2f2fd2de613df Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Sun, 5 Oct 2025 23:27:04 -0400
Subject: [PATCH 14/70] Renamed some dumb variables, started wishlist
---
Server/webbyBits.py | 20 ++++++++------------
wishlist.md | 12 ++++++++++++
2 files changed, 20 insertions(+), 12 deletions(-)
create mode 100644 wishlist.md
diff --git a/Server/webbyBits.py b/Server/webbyBits.py
index c10a04b..a70fb93 100644
--- a/Server/webbyBits.py
+++ b/Server/webbyBits.py
@@ -11,7 +11,6 @@ parser.add_argument('-p','--port',help="Port to host on, not the same as the web
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="")
args = parser.parse_args()
-
portTheUserPicked=args.port
# Just a note that the return code "401" as of now is used to mean "you don't have the password"
# This is not great design, and the whole "returning string codes" thing is something to add to the todo list
@@ -59,8 +58,8 @@ songNext = None
skipNow = False
playlist = []
playlistLock = threading.Lock()
-fakeplayer = vlc.Instance()
-player = fakeplayer.media_player_new()
+vlcInstance = vlc.Instance()
+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__)
@@ -77,17 +76,17 @@ def playQueuedSongs():
global partyMode
while True:
with playlistLock:
- z = str(player.get_state())
-
- if playlist and (z == "State.Ended" or z== "State.Stopped" or z == "State.NothingSpecial" or skipNow == True):
+ playerState = str(player.get_state())
+ endStates = ["State.Ended","State.Stopped","State.NothingSpecial"]
+ if playlist and (playerState in endStates or skipNow == True):
# New song is in the queue and (the previous song is over or skip has been pressed)
player.stop()
skipNow = False
songNext = playlist.pop(0)
- media = fakeplayer.media_new(soundLocation+songNext)
+ media = vlcInstance.media_new(soundLocation+songNext)
player.set_media(media)
player.play()
- elif (skipNow==True or (z == "State.Ended" or z == "State.NothingSpecial" or z=="State.Stopped")):
+ elif (skipNow==True or (playerState in endStates)):
# skip was pressed and there are no new songs
skipNow=False
songNext = None
@@ -112,7 +111,6 @@ queueThread.start()
def playerControls():
# recieve control inputs (play/pause and skip) from the webUI
global skipNow
- global media
global partyMode
recieveData=request.get_json(force=True)
if recieveData["control"] != None:
@@ -153,8 +151,6 @@ def settingsControl():
else:
return ERR_NO_ADMIN
elif recieveData["setting"] == "perms":
- # print(ADMIN_PASS)
- # print(recieveData["password"])
if ADMIN_PASS == recieveData["password"] and ADMIN_PASS:
#if an adminpass doesn't exist these perms can never be changed
controlPerms = recieveData["admin"]
@@ -201,7 +197,7 @@ def songadd():
queueSong(recieveData['song'])
return "200"
else:
- # Pass exists, or this action isn't restricted
+ # Pass exists, and the action is restricted
return ERR_NO_ADMIN
@app.route("/playlist", methods=["POST"])
diff --git a/wishlist.md b/wishlist.md
new file mode 100644
index 0000000..722cc86
--- /dev/null
+++ b/wishlist.md
@@ -0,0 +1,12 @@
+## Wishlist
+*Features I would like to add, will be completed in any order*
+- [x] Admin password
+ * Allows restricting certain features and changing permissions on the fly on the client
+- [ ] Refactoring existing code
+ - [ ] Update the SQL -> Server -> Client pipeline when searching and building playlist
+- [ ] Secure Password
+ * Actually use SSL for stuff that should be using it
+- [ ] GUI update for client
+ - [ ] Google material design??
+ - [ ] Dark mode?
+ - [ ] New Icons
\ No newline at end of file
From e08e9cbcca1110d618d00d41cf6b9e99335951ac Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Mon, 6 Oct 2025 12:47:58 -0400
Subject: [PATCH 15/70] Fixed a bunch of bugs to make adminpass complete
also added a bunch of comments and a new item design (css) that i'm probably going to undo
---
Client/scripts.js | 32 +++++++++++++++++++-------------
Client/styles.css | 8 ++++++--
Server/webbyBits.py | 7 +++++--
wishlist.md | 9 ++++++++-
4 files changed, 38 insertions(+), 18 deletions(-)
diff --git a/Client/scripts.js b/Client/scripts.js
index 46d1a54..3db547d 100644
--- a/Client/scripts.js
+++ b/Client/scripts.js
@@ -1,3 +1,4 @@
+// set all the global stuff
let ip;
let alertTime = 2;
let adminPass = "";
@@ -26,7 +27,11 @@ async function getFromServer(bodyInfo, source="",password=adminPass) {
}
});
const data = await response.json();
- if (data == "401") {
+ if (data == ERR_NO_ADMIN) {
+ // im suprised i didn't comment on this already but this is kinda lame desing
+ // its not wrong but you know
+ // it is easy which i like
+ // and it overrides any other non-async alerts which is nice
alertText("Error: Admin restricted action")
}
return await data;
@@ -36,14 +41,13 @@ async function getFromServer(bodyInfo, source="",password=adminPass) {
} else if(e == "") {
} else {
- alertText("error: " + e)
+ alertText("Error: " + e)
}
const response=null;
return response;
}
}
-
//cookie reader is taken from internet because cookies ae too complicated for me
//i still understand how it works though promise just i see no reason to write this from scratch
function getCookie(cname) {
@@ -63,34 +67,34 @@ function getCookie(cname) {
}
//someone more organised than me would have set all these html elements to variables so they dont have to get them 50 times
async function controlButton(buttonType) {
- if (buttonType == "pp") {
+ if (buttonType == "pp") { // Play-Pause button
getFromServer({control: "play-pause"}, "controls")
- } else if (buttonType == "sk") {
+ } else if (buttonType == "sk") { // Skip button
getFromServer({control: "skip"}, "controls")
if (document.getElementById("playlist-mode").style.display == "block") {
generateVisualPlaylist("skip-button");
}
- } else if (buttonType == "pl") {
+ } else if (buttonType == "pl") { // Playlist button
document.getElementById("songlist").innerHTML = "";
document.getElementById("playlist").innerHTML = "";
document.getElementById("playlist-mode").style.display = "block";
document.getElementById("songlist-mode").style.display = "none";
document.getElementById("settings-mode").style.display = "none";
generateVisualPlaylist();
- } else if (buttonType == "se") {
+ } else if (buttonType == "se") { //SearchMode button
document.getElementById("songlist").innerHTML = "
Search to find songs!
";
document.getElementById("playlist").innerHTML = "";
document.getElementById("playlist-mode").style.display = "none";
document.getElementById("songlist-mode").style.display = "block";
document.getElementById("settings-mode").style.display = "none";
- } else if (buttonType == "st") {
+ } else if (buttonType == "st") { //Settings button
document.getElementById("songlist").innerHTML = "";
document.getElementById("playlist").innerHTML = "";
document.getElementById("playlist-mode").style.display = "none";
document.getElementById("songlist-mode").style.display = "none";
document.getElementById("settings-mode").style.display = "block";
checkSettings()
- } else if (buttonType = "pm") {
+ } else if (buttonType = "pm") { //Partymode toggle (in settings)
await getFromServer({setting: "partymode-toggle"}, "settings")
checkSettings(true)
}
@@ -105,7 +109,6 @@ function searchSongsEnter(e) {
}
async function searchSongs(searchTerm){
- let optionslist = []
document.getElementById("songlist").innerHTML = ""
searchResults = await getFromServer({search:searchTerm},"search").then()
//generate the visual song list
@@ -345,7 +348,7 @@ async function submitPerms(e) {
tempData["SK"] = document.getElementById("skipsongsettingcheckbox").checked;
tempData["AS"] = document.getElementById("addsongsettingcheckbox").checked;
tempData["PM"] = document.getElementById("partymodesettingcheckbox").checked;
- tempData["VOL"] = document.getElementById("partymodesettingcheckbox").checked;
+ tempData["VOL"] = document.getElementById("volumechangesettingcheckbox").checked;
let returncode = await getFromServer({"setting":"perms","admin":tempData},"settings");
if (returncode == ERR_NO_ADMIN || returncode == null) {
// if you aren't allowed to check the box then toggle it again
@@ -366,10 +369,13 @@ document.addEventListener('keydown', function(e){
}})
document.getElementById("playlist-mode").style.display = "none";
document.getElementById("settings-mode").style.display = "none";
-//.ontouch for mobile??
document.getElementById("volumerange").onchange = async function() {
+ // there is no reason for this not to be a defined function
+ // FIX THIS
let returnValue = await getFromServer({setting:"volume",level:this.value}, "settings")
- if (returnValue["volumePassed"] !=0) {
+ if (returnValue == ERR_NO_ADMIN) {
+ alertText("Error: Admin restricted action");
+ } else if (returnValue["volumePassed"] !=0) {
// i forgot about this, i had to do this because it confused the crap out of me one time
// vlc doesn't let you change the volume of nothing, which makes sense if you think about it
alertText("Nothing is playing")
diff --git a/Client/styles.css b/Client/styles.css
index a6c4cdf..fdd3d09 100644
--- a/Client/styles.css
+++ b/Client/styles.css
@@ -72,6 +72,11 @@ h4 {
text-align: center;
}
+.item {
+ /* Only actually applies to playlist and search because settings item has "inherit" bg-colour */
+ background-color: #DDDDDD;
+}
+
/* Songlist stuff */
.songlist {
width: 80%;
@@ -87,7 +92,6 @@ h4 {
max-width: 150px;
margin: 5px auto;
min-width: 75px;
- background-color: inherit;
}
.songlist > .item > img{
@@ -160,7 +164,7 @@ h4 {
.settings > .item {
margin-left: 10%;
width:fit-content;
-
+ background-color: inherit;
}
.settings > .item:not(:last-child) {
diff --git a/Server/webbyBits.py b/Server/webbyBits.py
index a70fb93..590ccad 100644
--- a/Server/webbyBits.py
+++ b/Server/webbyBits.py
@@ -20,6 +20,9 @@ ADMIN_PASS = args.admin
if not(ADMIN_PASS):
ADMIN_PASS = None
# True = everyone, False = admin only. Change in client while in use.
+"""PP,SK,AS,PM,VOL all set to True or False
+False is admin only
+True is all users"""
controlPerms = {
"PP":True, #done
"SK":True, #done
@@ -192,12 +195,12 @@ def searchSongDB():
@app.route("/songadd", methods=["POST"])
def songadd():
recieveData=request.get_json(force=True)
- if (ADMIN_PASS and ADMIN_PASS == recieveData['password']):
+ if (ADMIN_PASS and ADMIN_PASS == recieveData['password']) or controlPerms["AS"]:
# Pass exists and is correct, or it's not restricted
queueSong(recieveData['song'])
return "200"
else:
- # Pass exists, and the action is restricted
+ # the pass is incorrect (technically a pass not existing falls into the above case because controlPerms is never changed)
return ERR_NO_ADMIN
@app.route("/playlist", methods=["POST"])
diff --git a/wishlist.md b/wishlist.md
index 722cc86..75ec375 100644
--- a/wishlist.md
+++ b/wishlist.md
@@ -4,9 +4,16 @@
* Allows restricting certain features and changing permissions on the fly on the client
- [ ] Refactoring existing code
- [ ] Update the SQL -> Server -> Client pipeline when searching and building playlist
+ - [ ] Verify all if-else sequences are correct and not redundant
+ - [ ] Remove old comments
- [ ] Secure Password
* Actually use SSL for stuff that should be using it
- [ ] GUI update for client
- [ ] Google material design??
- [ ] Dark mode?
- - [ ] New Icons
\ No newline at end of file
+ - [ ] New Icons
+- [ ] "Credit" system so each client can only add a set number of songs
+ - Based on time period, number in queue, other possible ideas for credits
+ - Without a login system there's no easy way to give credits to specific clients (and a login is beyond scope of what I want to do)
+ - Potentially a "redemption code" system, which can be tracked client side
+ - All of this is also very hackable without a server-side login.
\ No newline at end of file
From dcfe7115fa4259cad268ee336d9a46f9d37396ff Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Tue, 7 Oct 2025 16:36:06 -0400
Subject: [PATCH 16/70] Playlist items text is spaced not stupidly
Playlist Items look like not garbage now
I gave them a giant margin and forgot about it. should be good now
the item holders have padding instead for the edges
---
Client/styles.css | 3 ++-
Server/databaseGenerator.py | 2 --
wishlist.md | 3 ++-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/Client/styles.css b/Client/styles.css
index fdd3d09..ffe8a7d 100644
--- a/Client/styles.css
+++ b/Client/styles.css
@@ -136,6 +136,7 @@ h4 {
}
.playlist > .item > .text {
+ padding: 3px;
display: inline-block;
margin: 0px 3px;
}
@@ -150,7 +151,7 @@ h4 {
}
.playlist > .item > .text > * {
- margin:5% 2px;
+ margin:2px;
}
/* settings stuff */
diff --git a/Server/databaseGenerator.py b/Server/databaseGenerator.py
index e83cbc3..8a9b714 100644
--- a/Server/databaseGenerator.py
+++ b/Server/databaseGenerator.py
@@ -108,6 +108,4 @@ for i in songFiles:
# each "song" is stored as a dictionary/JSON entry following the format seen in the readME
songDatabase.execute(f"INSERT INTO songs (filename, title, artist, art, length) VALUES (?,?,?,?,?)",(i,title,artist,image,length))
-
-
fileOfDB.commit()
diff --git a/wishlist.md b/wishlist.md
index 75ec375..bb3161d 100644
--- a/wishlist.md
+++ b/wishlist.md
@@ -9,6 +9,7 @@
- [ ] Secure Password
* Actually use SSL for stuff that should be using it
- [ ] GUI update for client
+ - [x] Playlist items look cleaner
- [ ] Google material design??
- [ ] Dark mode?
- [ ] New Icons
@@ -16,4 +17,4 @@
- Based on time period, number in queue, other possible ideas for credits
- Without a login system there's no easy way to give credits to specific clients (and a login is beyond scope of what I want to do)
- Potentially a "redemption code" system, which can be tracked client side
- - All of this is also very hackable without a server-side login.
\ No newline at end of file
+ - All of this is very hackable without a server-side login.
\ No newline at end of file
From b20f0ecad0bb5b59a372e50a97b1d5923b578a8f Mon Sep 17 00:00:00 2001
From: Kristy Fournier <124598538+kristy-fournier@users.noreply.github.com>
Date: Fri, 10 Oct 2025 15:28:49 -0400
Subject: [PATCH 17/70] Client is now 100% hosted on the included files
no more web references (so you don't need internet access on a closed network)
---
Client/ext/popper.js | 134 +++++++++++++++++++++++++++++++++++++++
Client/ext/qrcode.min.js | 1 +
Client/index.html | 16 ++---
readme.md | 4 +-
4 files changed, 145 insertions(+), 10 deletions(-)
create mode 100644 Client/ext/popper.js
create mode 100644 Client/ext/qrcode.min.js
diff --git a/Client/ext/popper.js b/Client/ext/popper.js
new file mode 100644
index 0000000..19a42b9
--- /dev/null
+++ b/Client/ext/popper.js
@@ -0,0 +1,134 @@
+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 =
+ '
';
+
+ 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/Client/ext/qrcode.min.js b/Client/ext/qrcode.min.js
new file mode 100644
index 0000000..993e88f
--- /dev/null
+++ b/Client/ext/qrcode.min.js
@@ -0,0 +1 @@
+var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=['
'],h=0;d>h;h++){g.push("
");for(var i=0;d>i;i++)g.push('
');g.push("
")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}();
\ No newline at end of file
diff --git a/Client/index.html b/Client/index.html
index 8bd1d0a..6ee33ca 100644
--- a/Client/index.html
+++ b/Client/index.html
@@ -8,7 +8,7 @@
-
+
Jukebox Remote
@@ -27,7 +27,7 @@ changes visibility with JS-->
These are generated using javascript for search
-
+
Song title
Artist
@@ -83,8 +83,6 @@ changes visibility with JS-->
Share the remote:
-
Admin Settings
@@ -111,13 +109,13 @@ changes visibility with JS-->