diff --git a/Client/index.html b/Client/index.html index a45c6b1..d2e4234 100644 --- a/Client/index.html +++ b/Client/index.html @@ -6,6 +6,7 @@ + diff --git a/Client/scripts.js b/Client/scripts.js index 7435742..e8b7604 100644 --- a/Client/scripts.js +++ b/Client/scripts.js @@ -8,6 +8,7 @@ const VALID_FILE_EXT = ["mp3","flac","wav"]; let playlistTimeTimer=null; let playlistElapsedSeconds=0; let playlistSongLength=-1; +let currentlyPlaying = false; const params = new URLSearchParams(location.search); @@ -101,10 +102,12 @@ function getCookie(cname) { //someone more organised than me would have set all these html elements to variables so they dont have to get them 50 times // also someone who likes things not being dumb more than me would have separated the client and server buttons async function controlButton(buttonType) { - clearInterval(playlistTimeTimer); if (buttonType == "pp") { // Play-Pause button - getFromServer({control: "play-pause"}, "controls") + let result = await getFromServer({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"); console.log(returnCode["ok"]) if(returnCode["ok"]) { @@ -113,6 +116,7 @@ async function controlButton(buttonType) { } } } else if (buttonType == "pl") { // Playlist button + clearInterval(playlistTimeTimer); document.getElementById("songlist").innerHTML = ""; document.getElementById("playlist").innerHTML = "

"; document.getElementById("playlist-mode").style.display = "block"; @@ -120,12 +124,14 @@ async function controlButton(buttonType) { document.getElementById("settings-mode").style.display = "none"; generateVisualPlaylist(); } else if (buttonType == "se") { //SearchMode button + clearInterval(playlistTimeTimer); 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") { //Settings button + clearInterval(playlistTimeTimer); document.getElementById("songlist").innerHTML = ""; document.getElementById("playlist").innerHTML = ""; document.getElementById("playlist-mode").style.display = "none"; @@ -251,19 +257,28 @@ function qrCodeGenerate() { }); } -async function displayElapsedPlaylistTime(elapsed=0,length=0) { - if(Math.floor(elapsed) === Math.floor(length)){ - console.log("somethingShouldBeHappening") - playlistElapsedSeconds = 0; - generateVisualPlaylist(); +async function displayElapsedPlaylistTime(elapsed=0,length=-1) { + if(currentlyPlaying) { + if(Math.floor(elapsed) > Math.floor(length) && typeof length === "number" && typeof elapsed === "number"){ + // console.log("somethingShouldBeHappening") + playlistElapsedSeconds = 0; + generateVisualPlaylist(); + } + let mins = Math.floor(elapsed/60); + let secs = Math.floor(elapsed%60); + let durMins = Math.floor(length/60); + let durSecs = Math.floor(length%60); + let timeLeft = document.getElementById("elapsed-time-display"); + if(mins > durMins) { + mins = durMins; + if(secs > durSecs) { + secs = durSecs; + } + } + + timeLeft.innerHTML = mins.toString() +":"+ secs.toLocaleString('en-US', {minimumIntegerDigits: 2,useGrouping: false}) + "/"+ durMins.toString()+":"+durSecs.toLocaleString('en-US', {minimumIntegerDigits: 2,useGrouping: false}); + // playlistElapsedSeconds++; } - let mins = Math.floor(elapsed/60); - let secs = Math.floor(elapsed%60); - let durMins = Math.floor(length/60); - let durSecs = Math.floor(length%60); - let timeLeft = document.getElementById("elapsed-time-display"); - timeLeft.innerHTML = mins.toString() +":"+ secs.toLocaleString('en-US', {minimumIntegerDigits: 2,useGrouping: false}) + "/"+ durMins.toString()+":"+durSecs.toLocaleString('en-US', {minimumIntegerDigits: 2,useGrouping: false}); - playlistElapsedSeconds++; } async function checkSettings(skipServer=false) { @@ -313,7 +328,8 @@ async function checkSettings(skipServer=false) { async function generateVisualPlaylist(conditions="") { document.getElementById("playlist").innerHTML = "

"; data = await getFromServer(null, "playlist"); - playlist = data["data"]; + playlist = data["data"]["playlist"]; + currentlyPlaying = data["data"]["playingState"] playlist = Object.values(playlist).map(obj => { const filename = Object.keys(obj)[0]; // Get the filename const songData = obj[filename]; // Get the song metadata @@ -323,7 +339,7 @@ async function generateVisualPlaylist(conditions="") { clearInterval(playlistTimeTimer); document.getElementById("playlist-alert").innerHTML = "Nothing's Queued..." } else { - if (conditions=="skip-button") { + if (conditions==="skip-button") { playlist.shift() if (playlist.length==0){ document.getElementById("playlist-alert").innerHTML = "Nothing's Queued..." @@ -375,9 +391,6 @@ async function generateVisualPlaylist(conditions="") { playlistSongLength = playlist[0]["length"]; displayElapsedPlaylistTime(playlistElapsedSeconds,playlistSongLength); clearInterval(playlistTimeTimer); - playlistTimeTimer = setInterval(() => { - displayElapsedPlaylistTime(playlistElapsedSeconds,playlistSongLength); - },1000) } } }catch(err){ @@ -386,6 +399,9 @@ async function generateVisualPlaylist(conditions="") { } } } + playlistTimeTimer = setInterval(() => { + displayElapsedPlaylistTime(playlistElapsedSeconds,playlistSongLength); + },1000) } async function submitSong(songid) { @@ -565,4 +581,26 @@ if (alertTime == "") { document.cookie = "alertTime="+alertTime+"; path=/;" } // this is the code that makes the qr code at the very start -qrCodeGenerate() \ No newline at end of file +qrCodeGenerate() + +// socket testing stuff + +socket = io("http://"+ip,{ + reconnectionAttemps: 5, + timeout: 10000, +}); + +socket.on("songAdd", function(data) { + console.log("recieved data from songAdd"); + console.log(data); + generateVisualPlaylist(); +}) + +socket.on("timeUpdate", function(data) { + console.log("recieved data from timeUpdate"); + console.log(data); + playlistElapsedSeconds = data["elapsedTime"]; + playingState = data["playingState"] +}); + +socket.on("skipSong",generateVisualPlaylist) \ No newline at end of file diff --git a/Server/webbyBits.py b/Server/webbyBits.py index ea876d8..02636da 100644 --- a/Server/webbyBits.py +++ b/Server/webbyBits.py @@ -1,6 +1,7 @@ from flask import Flask from flask import request from flask_cors import CORS +from flask_socketio import SocketIO import sqlite3 as sql import vlc,threading,time,random,argparse,dotenv,os,hashlib,string # Argparse Stuff @@ -64,20 +65,46 @@ 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="*") def queueSong(song): with playlistLock: playlist.append(song) + socketio.emit("songAdd",getSongInfo(song)) + +def getSongInfo(song): + fileofDB = sql.connect("songDatabase.db") + songDatabase = fileofDB.cursor() + songDatabase.execute("SELECT * FROM songs WHERE filename = ?",[song]) + result = songDatabase.fetchall()[0] + # again, this is still using the old JSON format to avoid client changes + k = { + "title": result[1], + "artist": result[2], + "art": result[3], + "length": result[4] + } + fileofDB.close() + return {song:k} # this is a loop that plays the songs and checks for playlist changes, skips, ect. +counter = 0 def playQueuedSongs(): global skipNow global songNext global partyMode + global counter while True: with playlistLock: + counter+=1 + if(counter > 10): + playingState = str(player.get_state()) == "State.Playing" + socketio.emit('timeUpdate',{"elapsedTime":player.get_time()/1000,"playingState":playingState}) playerState = str(player.get_state()) endStates = ["State.Ended","State.Stopped","State.NothingSpecial"] + if playerState == "State.Ended": + socketio.emit("skipSong",None) 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() @@ -87,6 +114,7 @@ def playQueuedSongs(): player.set_media(media) player.play() elif (skipNow==True or (playerState in endStates)): + # print(playerState) # skip was pressed and there are no new songs skipNow=False songNext = None @@ -99,11 +127,16 @@ def playQueuedSongs(): # adds the random songs for party mode # the above 2 means this only applies if (a song is playing or paused) and (the queue is empty) playlist.append(result[0][0]) + socketio.emit('songAdd',getSongInfo(result[0][0])) # 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.on("connect") +def handleConnect(): + pass + @app.route("/controls", methods=['POST']) def playerControls(): # recieve control inputs (play/pause and skip) from the webUI @@ -113,10 +146,12 @@ def playerControls(): try: if recieveData["control"] == "play-pause": if ADMIN_PASS == recieveData['password'] or controlPerms["PP"]: + playingState = str(player.get_state())=="State.Playing" player.pause() - return ERR_200 + return {"error":"ok","data":{"playingState":not(playingState)}},200 else: - return ERR_NO_ADMIN + 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"]: skipNow = True @@ -163,7 +198,7 @@ def settingsControl(): elif recieveData["setting"] == "perms": if ADMIN_PASS == recieveData["password"]: controlPerms = recieveData["admin"] - print(recieveData["admin"]) + # print(recieveData["admin"]) return ERR_200 else: return ERR_NO_ADMIN @@ -261,8 +296,11 @@ def getPlaylist(): } tempPlaylist.append({i:k}) fileofDB.close() - - return {"error":"ok","data":tempPlaylist} + playingState = False + if(str(player.get_state())=="State.Playing"): + playingState = True + # print(playingState) + return {"error":"ok","data":{"playlist":tempPlaylist,"playingState":playingState}},200 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 @@ -271,5 +309,5 @@ if __name__ == "__main__": queueThread = threading.Thread(target=playQueuedSongs) queueThread.daemon = True queueThread.start() - app.run(host='0.0.0.0', 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 e1324e1..48f75e6 100644 --- a/wishlist.md +++ b/wishlist.md @@ -1,7 +1,5 @@ ## 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 - [x] Remove old comments - [ ] Update the SQL -> Server -> Client pipeline when searching and building playlist @@ -24,7 +22,11 @@ - Potentially a "redemption code" system, which can be tracked client side - All of this is very hackable without a server-side login. - [ ] Websockets / some method of updating the time remaining to any client on the playlist screen - * currently the screen just grabs the "elapsed time" once when it is loaded - * websockets can re-update clients - * not actually sure if i can CORS-socket but we're sure gonna try - - [ ] Set a timeout to change the time \ No newline at end of file + * This is implemented in a very broken way right now + - [x] Set a timeout to change the time (to start) + - [x] Send updates to the playlist in real time when songs are added + * This is only kind of done, still needs work + - [ ] Update the playlist's html without destroying it (create 1 new element) + - [x] Tell clients looking at the playlist when the song has been paused (so they can pause the local timers) + * Again, still needs work + * This is currently solved by just sending the time and "playing status" once a second-ish \ No newline at end of file