Sockets allowing for live updates rather than complete rebuilds #7

Merged
kristy-fournier merged 20 commits from timerChanges into main 2026-02-11 09:39:36 -05:00
4 changed files with 111 additions and 32 deletions
Showing only changes of commit f064183b9a - Show all commits

View file

@ -6,6 +6,7 @@
<link rel="stylesheet" href="styles.css">
<link rel="manifest" href="manifest.json" />
<meta charset="utf-8">
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
</head>
<body id="test-body">
<!--Cookie Popup(does it matter if im not tracking them? i have no idea)-->

View file

@ -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");
copilot-pull-request-reviewer[bot] commented 2026-02-11 09:28:36 -05:00 (Migrated from github.com)

else if (buttonType = "pm") assigns instead of comparing, so this branch will always execute and also mutates buttonType. Change this to a comparison (===) so only the intended button triggers the party mode toggle.

    } else if (buttonType === "pm") { //Partymode toggle (in settings)
`else if (buttonType = "pm")` assigns instead of comparing, so this branch will always execute and also mutates `buttonType`. Change this to a comparison (`===`) so only the intended button triggers the party mode toggle. ```suggestion } else if (buttonType === "pm") { //Partymode toggle (in settings) ```
console.log(returnCode["ok"])
if(returnCode["ok"]) {
copilot-pull-request-reviewer[bot] commented 2026-02-11 09:28:38 -05:00 (Migrated from github.com)

In the non-OK response branch, throw new Error(data.error) makes the subsequent alertText(...) line unreachable, and also forces callers into the catch path even though you already have the server-provided error payload. Consider either removing the throw and returning the structured { ok/status/error } response, or moving the alert before throwing and ensuring callers handle a null return without crashing.


In the non-OK response branch, `throw new Error(data.error)` makes the subsequent `alertText(...)` line unreachable, and also forces callers into the catch path even though you already have the server-provided error payload. Consider either removing the throw and returning the structured `{ ok/status/error }` response, or moving the alert before throwing and ensuring callers handle a `null` return without crashing. ```suggestion ```
@ -113,6 +116,7 @@ async function controlButton(buttonType) {
}
}
} else if (buttonType == "pl") { // Playlist button
clearInterval(playlistTimeTimer);
document.getElementById("songlist").innerHTML = "";
document.getElementById("playlist").innerHTML = "<h1 id=\"playlist-alert\"></h1>";
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 = "<h1>Search to find songs!</h1>";
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 = "<h1 id=\"playlist-alert\"></h1>";
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) {
@ -566,3 +582,25 @@ if (alertTime == "") {
}
// this is the code that makes the qr code at the very start
qrCodeGenerate()
// socket testing stuff
socket = io("http://"+ip,{
reconnectionAttemps: 5,
timeout: 10000,
});
socket.on("songAdd", function(data) {
console.log("recieved data from songAdd");
copilot-pull-request-reviewer[bot] commented 2026-02-11 09:28:39 -05:00 (Migrated from github.com)

skipInPlaylist() assumes playlistChildren[1] exists and calls displayElapsedPlaylistTime() even when the playlist DOM (and elapsed-time-display) may not exist (e.g., when not in playlist mode or the playlist was cleared). This can throw on socket-driven skipSong events. Add guards for playlist-mode visibility/child existence and update playlistSongLength/playlist state when advancing to the next song so the timer stays correct.

    // reset elapsed time for the (new) current song
    playlistElapsedSeconds = 0;

    // Keep the in-memory playlist and song length in sync when skipping
    if (Array.isArray(typeof playlist !== "undefined" ? playlist : null) && playlist.length > 0) {
        // Remove the song that was just skipped
        playlist.shift();
        if (playlist.length > 0) {
            const nextSong = playlist[0];
            let nextLength = -1;
            if (nextSong && typeof nextSong.length === "number") {
                nextLength = nextSong.length;
            } else if (nextSong && typeof nextSong.duration === "number") {
                nextLength = nextSong.duration;
            }
            playlistSongLength = nextLength;
        } else {
            // No more songs queued
            playlistSongLength = -1;
            if (playlistTimeTimer) {
                clearInterval(playlistTimeTimer);
            }
        }
    }

    const playlistElement = document.getElementById("playlist");
    if (!playlistElement) {
        // Playlist UI is not present; nothing more to update visually.
        const elapsedDisplay = document.getElementById("elapsed-time-display");
        if (typeof displayElapsedPlaylistTime === "function" && elapsedDisplay) {
            displayElapsedPlaylistTime(playlistElapsedSeconds, playlistSongLength);
        }
        return;
    }

    let playlistChildren = playlistElement.children;

    // Safely remove the first actual song entry, if it exists
    if (playlistChildren.length > 1 && playlistChildren[1].nodeName === "DIV") {
        playlistChildren[1].remove();
        playlistChildren = playlistElement.children;
    }

    if (playlistChildren.length === 0) {
        // No children at all; nothing further to do
    } else if (playlistChildren.length === 1) {
        // Only the alert/header element remains
        playlistChildren[0].innerText = "Nothing's Queued...";
    } else {
        // There is a new first song entry; carefully reassign IDs
        const firstEntry = playlistChildren[1];
        if (firstEntry && firstEntry.children && firstEntry.children.length > 1) {
            const textContainer = firstEntry.children[1];
            if (textContainer && textContainer.children && textContainer.children.length > 3) {
                let firstElementTextChildren = textContainer.children;
                // console.log(firstElementTextChildren);
                firstElementTextChildren[2].id = "elapsed-time-display";
                firstElementTextChildren[3].id = "playing-indicator-text";
                firstElementTextChildren[3].textContent = "Playing";
            }
        }
    }

    const elapsedDisplay = document.getElementById("elapsed-time-display");
    if (typeof displayElapsedPlaylistTime === "function" && elapsedDisplay) {
        displayElapsedPlaylistTime(playlistElapsedSeconds, playlistSongLength);
    }
`skipInPlaylist()` assumes `playlistChildren[1]` exists and calls `displayElapsedPlaylistTime()` even when the playlist DOM (and `elapsed-time-display`) may not exist (e.g., when not in playlist mode or the playlist was cleared). This can throw on socket-driven `skipSong` events. Add guards for playlist-mode visibility/child existence and update `playlistSongLength`/`playlist` state when advancing to the next song so the timer stays correct. ```suggestion // reset elapsed time for the (new) current song playlistElapsedSeconds = 0; // Keep the in-memory playlist and song length in sync when skipping if (Array.isArray(typeof playlist !== "undefined" ? playlist : null) && playlist.length > 0) { // Remove the song that was just skipped playlist.shift(); if (playlist.length > 0) { const nextSong = playlist[0]; let nextLength = -1; if (nextSong && typeof nextSong.length === "number") { nextLength = nextSong.length; } else if (nextSong && typeof nextSong.duration === "number") { nextLength = nextSong.duration; } playlistSongLength = nextLength; } else { // No more songs queued playlistSongLength = -1; if (playlistTimeTimer) { clearInterval(playlistTimeTimer); } } } const playlistElement = document.getElementById("playlist"); if (!playlistElement) { // Playlist UI is not present; nothing more to update visually. const elapsedDisplay = document.getElementById("elapsed-time-display"); if (typeof displayElapsedPlaylistTime === "function" && elapsedDisplay) { displayElapsedPlaylistTime(playlistElapsedSeconds, playlistSongLength); } return; } let playlistChildren = playlistElement.children; // Safely remove the first actual song entry, if it exists if (playlistChildren.length > 1 && playlistChildren[1].nodeName === "DIV") { playlistChildren[1].remove(); playlistChildren = playlistElement.children; } if (playlistChildren.length === 0) { // No children at all; nothing further to do } else if (playlistChildren.length === 1) { // Only the alert/header element remains playlistChildren[0].innerText = "Nothing's Queued..."; } else { // There is a new first song entry; carefully reassign IDs const firstEntry = playlistChildren[1]; if (firstEntry && firstEntry.children && firstEntry.children.length > 1) { const textContainer = firstEntry.children[1]; if (textContainer && textContainer.children && textContainer.children.length > 3) { let firstElementTextChildren = textContainer.children; // console.log(firstElementTextChildren); firstElementTextChildren[2].id = "elapsed-time-display"; firstElementTextChildren[3].id = "playing-indicator-text"; firstElementTextChildren[3].textContent = "Playing"; } } } const elapsedDisplay = document.getElementById("elapsed-time-display"); if (typeof displayElapsedPlaylistTime === "function" && elapsedDisplay) { displayElapsedPlaylistTime(playlistElapsedSeconds, playlistSongLength); } ```
console.log(data);
generateVisualPlaylist();
})
socket.on("timeUpdate", function(data) {
console.log("recieved data from timeUpdate");
console.log(data);
copilot-pull-request-reviewer[bot] commented 2026-02-11 09:28:39 -05:00 (Migrated from github.com)

Variable playlist is used like a local variable, but is missing a declaration.

Variable playlist is used like a local variable, but is missing a declaration.
playlistElapsedSeconds = data["elapsedTime"];
playingState = data["playingState"]
});
socket.on("skipSong",generateVisualPlaylist)

View file

@ -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")
copilot-pull-request-reviewer[bot] commented 2026-02-11 09:28:36 -05:00 (Migrated from github.com)

cors_allowed_origins="*" enables any website to open a socket connection to this server and receive real-time events (playlist/time/settings). If this is intended only for the hosted client, consider making allowed origins configurable (e.g., via .env) and defaulting to the client origin instead of *.

# Configure allowed origins for Socket.IO via environment variable to avoid allowing all origins by default.
cors_allowed_origins_env = os.getenv("CORS_ALLOWED_ORIGINS")
if cors_allowed_origins_env:
    cors_allowed_origins = [origin.strip() for origin in cors_allowed_origins_env.split(",") if origin.strip()]
else:
    # Default to None (no cross-origin access) instead of "*" for better security.
    cors_allowed_origins = None
socketio = SocketIO(app, cors_allowed_origins=cors_allowed_origins)
`cors_allowed_origins="*"` enables any website to open a socket connection to this server and receive real-time events (playlist/time/settings). If this is intended only for the hosted client, consider making allowed origins configurable (e.g., via `.env`) and defaulting to the client origin instead of `*`. ```suggestion # Configure allowed origins for Socket.IO via environment variable to avoid allowing all origins by default. cors_allowed_origins_env = os.getenv("CORS_ALLOWED_ORIGINS") if cors_allowed_origins_env: cors_allowed_origins = [origin.strip() for origin in cors_allowed_origins_env.split(",") if origin.strip()] else: # Default to None (no cross-origin access) instead of "*" for better security. cors_allowed_origins = None socketio = SocketIO(app, cors_allowed_origins=cors_allowed_origins) ```
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)

View file

@ -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
* 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