diff --git a/.gitignore b/.gitignore index 3920035..87c9ad3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ server/sound/ *.db start.bat -.env \ No newline at end of file +.env +venv/ \ No newline at end of file diff --git a/Client/index.html b/Client/index.html index 7172540..428fb2d 100644 --- a/Client/index.html +++ b/Client/index.html @@ -5,6 +5,7 @@ +
@@ -107,7 +108,7 @@ changes visibility with JS-->Wipe the playlist, except the currently playing song*
-PartyJukebox is under an AGPLV3 liscense. You can access the source code here.
+PartyJukebox is under an AGPLV3 liscense. You can access the source code here.
diff --git a/Client/scripts.js b/Client/scripts.js index 826709f..cb7361e 100644 --- a/Client/scripts.js +++ b/Client/scripts.js @@ -2,7 +2,7 @@ let ip; let alertTime = 2; let adminPass = ""; -const ERR_NO_ADMIN = "401"; // gonna use this later to refactor +const ERR_NO_ADMIN = 401; const VALID_FILE_EXT = ["mp3","flac","wav"]; const params = new URLSearchParams(location.search); @@ -38,7 +38,6 @@ async function getFromServer(bodyInfo, source="",password=adminPass) { // the currently set password is always included in every request bodyInfo["password"] = password; } - // console.log(bodyInfo); const response = await fetch("http://"+ip+"/"+source, { method: "POST", body: JSON.stringify(bodyInfo), @@ -46,20 +45,27 @@ async function getFromServer(bodyInfo, source="",password=adminPass) { "Content-type": "application/json; charset=UTF-8" } }); - const data = await response.json(); - if (data == ERR_NO_ADMIN) { + + let data = await response.json(); // original json + if (response.status == ERR_NO_ADMIN) { // im suprised i didn't comment on this already but this is kinda lame desing // its not wrong but you know // it is easy which i like // and it overrides any other non-async alerts which is nice alertText("Error: Admin restricted action") + } else if(!response.ok){ + alertText("Error: "+data.error); } + // we add some information from the response just in case it is needed + data["ok"] = response.ok; + data["status"] = response.status; + // console.log(data); return await data; } catch(e) { + // console.log("error print here:"); + // console.log(e); if (e == "TypeError: Failed to fetch"){ alertText("Error: Can't Connect to Server (is the ip set?)") - } else if(e == "") { - } else { alertText("Error: " + e); } @@ -86,6 +92,7 @@ function getCookie(cname) { return ""; } //someone more organised than me would have set all these html elements to variables so they dont have to get them 50 times +// also someone who likes things not being dumb more than me would have separated the client and server buttons async function controlButton(buttonType) { if (buttonType == "pp") { // Play-Pause button getFromServer({control: "play-pause"}, "controls") @@ -130,7 +137,8 @@ function searchSongsEnter(e) { async function searchSongs(searchTerm){ document.getElementById("songlist").innerHTML = "" - searchResults = await getFromServer({search:searchTerm},"search").then() + let fetchResults = await getFromServer({search:searchTerm},"search").then(); + let searchResults = fetchResults.data; //generate the visual song list for(var fileName in searchResults) { let currentSongInJSON = searchResults[fileName] @@ -155,6 +163,13 @@ async function searchSongs(searchTerm){ newItem.appendChild(image); newItem.appendChild(head3); newItem.appendChild(head4); + // I like this concept but i'm leaving it out for now + // if(currentSongInJSON.lossless === 1) { + // let losslesstag = document.createElement("p"); + // losslesstag.textContent = "Ⓛ"; + // losslesstag.classList.add("lossless-tag"); + // newItem.appendChild(losslesstag); + // } document.getElementById("songlist").appendChild(newItem); } @@ -237,7 +252,8 @@ async function checkSettings(skipServer=false) { } } //ping the server here - x = await getFromServer({setting: "getsettings"}, "settings"); + data = await getFromServer({setting: "getsettings"}, "settings"); + x = data["data"]; if (!(skipServer) || partyButtonState=="N/A") { if (x["partymode"] == false) { document.getElementById("partymode-button").innerHTML = "Off"; @@ -262,7 +278,8 @@ async function checkSettings(skipServer=false) { async function generateVisualPlaylist(conditions="") { document.getElementById("playlist").innerHTML = ""; - playlist = await getFromServer(null, "playlist"); + data = await getFromServer(null, "playlist"); + playlist = data["data"]; playlist = Object.values(playlist).map(obj => { const filename = Object.keys(obj)[0]; // Get the filename const songData = obj[filename]; // Get the song metadata @@ -301,7 +318,7 @@ async function generateVisualPlaylist(conditions="") { let timeLeft =document.createElement("h5"); timeLeft.style.fontWeight = 100; try { - if (i == 0) { + if (i == 0) { // Only the first song in the loop gets a time head5.innerHTML="Playing"; if ((conditions != "skip-button")) { let mins = Math.floor(playlist[i]["time"]/60); @@ -313,7 +330,7 @@ async function generateVisualPlaylist(conditions="") { } }catch(err){ // i dont know why there's a try catch here but i'm leaving it i dont want to break something - console.log(err) + console.error(err) } let textdiv = document.createElement("div") textdiv.className="text" @@ -332,11 +349,9 @@ async function submitSong(songid) { let returncode = await getFromServer({song: songid}, "songadd"); if(returncode == ERR_NO_ADMIN) { // right now the error is alerted in getFromServer, maybe will change that - } - else if(returncode["error"]=="song-in-queue") { + } else if(returncode["error"]=="song-in-queue") { alertText("That song's about to play! Hang on!") - } - else { + } else { alertText("Added to Queue"); } } @@ -346,11 +361,10 @@ function checkWhatSongWasClicked(e) { if ((itemId.length-itemId.lastIndexOf("image") == 5) && itemId.lastIndexOf("image")!=-1) { itemId = itemId.slice(0,-6) } + let filenameSep = itemId.split('.') //i feel like later kristy won't apreciate this //one of my files was "file.MP3" so it didn't work //windows be like - let filenameSep = itemId.split('.') - if (VALID_FILE_EXT.includes(filenameSep[filenameSep.length-1].toLowerCase())) { submitSong(itemId); } @@ -440,16 +454,17 @@ document.getElementById("volumerange").onchange = async function() { // there is no reason for this not to be a defined function // FIX THIS let returnValue = await getFromServer({setting:"volume",level:this.value}, "settings") - if (returnValue == ERR_NO_ADMIN) { + if (returnValue["status"] == ERR_NO_ADMIN) { // alertText("Error: Admin restricted action"); // there's an admin restrict alert built into getFromServer - } else if (returnValue["volumePassed"] !=0) { + // i wanna put the volume slider back to where it was but idk a good way to keep the previous volume + checkSettings(false); + } else if (returnValue["data"]["volumePassed"] !=0) { // i forgot about this, i had to do this because it confused the crap out of me one time // vlc doesn't let you change the volume of nothing, which makes sense if you think about it alertText("Nothing is playing") document.getElementById("volumerange").value = -1 - } - else if (this.value == 0) { + } else if (this.value == 0) { alertText("The volume is now set to 0 (Pause?)") } else { alertText("The volume is now set to " + this.value.toString()) diff --git a/Client/styles.css b/Client/styles.css index 024e988..21f7b7c 100644 --- a/Client/styles.css +++ b/Client/styles.css @@ -134,6 +134,7 @@ h4 { .songlist > .item > h3, .songlist > .item > h4{ margin-left: 2px; margin-right: 2px; + margin: 5px; word-wrap: break-word; } @@ -152,6 +153,12 @@ h4 { width: 20%; min-width: 50px; } + +.lossless-tag { + width:16px; + padding: 1px; + margin-left: auto; +} /* playlist mode stuff */ .playlist { diff --git a/Server/webbyBits.py b/Server/webbyBits.py index 130131b..d0d2e36 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -10,10 +10,10 @@ parser.add_argument('-a','--admin',help="Add an admin password to be used in the args = parser.parse_args() dotenv.load_dotenv() portTheUserPicked=os.getenv("SERVER_PORT") -# Just a note that the return code "401" as of now is used to mean "you don't have the password" -# This is not great design, and the whole "returning string codes" thing is something to add to the todo list -# I mean returning 200 when no return is necesary i think is fine but we'll see -ERR_NO_ADMIN = "401" + +ERR_NO_ADMIN = ({"error":"no-admin","data":None},401) +ERR_200 = ({"error":"OK","data":None},200) +ERR_MISSING_ARGS = ({"error":"Request missing required arguments","data":None}),400 if args.admin: ADMIN_PASS = hashlib.sha256(bytes(args.admin,'utf-8')).hexdigest() else: @@ -99,7 +99,8 @@ def playQueuedSongs(): # the above 2 means this only applies if (a song is playing or paused) and (the queue is empty) playlist.append(result[0][0]) # check for new songs every second - # I just didn't want to eat too much processing looping + # I just didn't want to eat too much processing looping + # this also has another useful affect that skips get "queued" to only 1 per second, that way somebody usually can't skip twice accidentally time.sleep(1) @app.route("/controls", methods=['POST']) @@ -108,30 +109,31 @@ def playerControls(): global skipNow global partyMode recieveData=request.get_json(force=True) - if recieveData["control"] != None: + try: if recieveData["control"] == "play-pause": if ADMIN_PASS == recieveData['password'] or controlPerms["PP"]: player.pause() - return "200" + return ERR_200 else: return ERR_NO_ADMIN elif recieveData["control"] == "skip": if ADMIN_PASS == recieveData['password'] or controlPerms["SK"]: skipNow = True - return "200" + return ERR_200 else: return ERR_NO_ADMIN + # Maybe i should have put this next one in the "settings" section elif recieveData["control"] == "clear": if ADMIN_PASS == recieveData['password']: # this is only ever allowed with the adminpassword with playlistLock: playlist.clear() - return "200" + return ERR_200 else: return ERR_NO_ADMIN else: - return "400" - else: - return "400" + return {"error":"Not a valid control","data":None},400 + except KeyError: + return ERR_MISSING_ARGS @app.route("/settings", methods=['POST']) def settingsControl(): @@ -140,71 +142,90 @@ def settingsControl(): global partyMode global player recieveData = request.get_json(force=True) - if recieveData["setting"] == "volume": - if ADMIN_PASS == recieveData['password'] or controlPerms["VOL"]: - volumePassed = player.audio_set_volume(int(recieveData["level"])) - return {"volumePassed":volumePassed} + try: + if recieveData["setting"] == "volume": + if ADMIN_PASS == recieveData['password'] or controlPerms["VOL"]: + volumeLevel = int(recieveData["level"]) + if(volumeLevel <= 100 and volumeLevel >= 0): + volumePassed = player.audio_set_volume(volumeLevel) + return {"error":"ok","data":{"volumePassed":volumePassed}},200 + else: + return {"error":"Invalid volume level","data":None},422 + else: + return ERR_NO_ADMIN + elif recieveData["setting"] == "partymode-toggle": + if ADMIN_PASS == recieveData['password'] or controlPerms["PM"]: + partyMode = not(partyMode) + return ERR_200 + else: + return ERR_NO_ADMIN + elif recieveData["setting"] == "perms": + if ADMIN_PASS == recieveData["password"]: + controlPerms = recieveData["admin"] + return ERR_200 + else: + return ERR_NO_ADMIN + elif recieveData["setting"] == "getsettings": + # probably should have made this a different request type or something but it works + return {"error":"ok","data":{"partymode":partyMode,"volume":player.audio_get_volume(),"admin":controlPerms}},200 else: - return ERR_NO_ADMIN - elif recieveData["setting"] == "partymode-toggle": - if ADMIN_PASS == recieveData['password'] or controlPerms["PM"]: - partyMode = not(partyMode) - return "200" - else: - return ERR_NO_ADMIN - elif recieveData["setting"] == "perms": - if ADMIN_PASS == recieveData["password"] and ADMIN_PASS: - #if an adminpass doesn't exist these perms can never be changed - controlPerms = recieveData["admin"] - return "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 {"partymode":partyMode,"volume":player.audio_get_volume(),"admin":controlPerms} - else: - return "400" + return {"error":"Not a valid setting","data":None},400 + except: + return ERR_MISSING_ARGS @app.route("/search", methods=['POST']) def searchSongDB(): recieveData=request.get_json(force=True) fileofDB = sql.connect("songDatabase.db") songDatabase = fileofDB.cursor() - results = [] - if (recieveData['search'] == ""): - songDatabase.execute("SELECT * FROM virtualSongs") - results = songDatabase.fetchall() - else: - songDatabase.execute("SELECT * FROM virtualSongs WHERE virtualSongs MATCH ?",[recieveData['search']]) - results = songDatabase.fetchall() - tempdata = {} - # this is a temporary solution so i dont have to change the client - for i in results: - tempdata[i[0]] = { - "title": i[1], - "artist": i[2], - "art": i[3], - "length": i[4] - } - fileofDB.close() - return tempdata + try: + results = [] + if (recieveData['search'] == ""): + songDatabase.execute("SELECT * FROM virtualSongs") + results = songDatabase.fetchall() + else: + songDatabase.execute("SELECT * FROM virtualSongs WHERE virtualSongs MATCH ?",[recieveData['search']]) + results = songDatabase.fetchall() + tempdata = {} + # this is a temporary solution so i dont have to change the client + for i in results: + tempdata[i[0]] = { + "title": i[1], + "artist": i[2], + "art": i[3], + "length": i[4], + "lossless":i[5] + } + fileofDB.close() + + return {"error":"ok","data":tempdata},200 + except KeyError: + fileofDB.close() + return ERR_MISSING_ARGS + except sql.OperationalError: + fileofDB.close() + return ({"error":"Invalid search, sorry!","data":None},422) + @app.route("/songadd", methods=["POST"]) def songadd(): recieveData=request.get_json(force=True) - if (ADMIN_PASS and ADMIN_PASS == recieveData['password']) or controlPerms["AS"]: - # Password exists and is correct, or it's not restricted - # if (recieveData['song'] in playlist): - # return {"error":"song-in-queue"} - # else: - # Right now the above is disabled since i want to make it optional first - # probably with a checkbox like the other admin controls - if True: - queueSong(recieveData['song']) - return "200" - else: - # the password is incorrect (technically a password not existing falls into the above case because controlPerms is never changed) - return ERR_NO_ADMIN + try: + if (ADMIN_PASS == recieveData['password']) or controlPerms["AS"]: + # Password exists and is correct, or it's not restricted + # if (recieveData['song'] in playlist): + # return {"error":"song-in-queue"} + # else: + # Right now the above is disabled since i want to make it optional first + # probably with a checkbox like the other admin controls + if True: + queueSong(recieveData['song']) + return ERR_200 + else: + # the password is incorrect (technically a password not existing falls into the above case because controlPerms is never changed) + return ERR_NO_ADMIN + except KeyError: + return ERR_MISSING_ARGS @app.route("/playlist", methods=["POST"]) def getPlaylist(): @@ -238,7 +259,8 @@ def getPlaylist(): } tempPlaylist.append({i:k}) fileofDB.close() - return tempPlaylist + + return {"error":"ok","data":tempPlaylist} if __name__ == "__main__": # There's not really a whole lot of point to a main function for something like this, you'd never use any of these methods