Sockets allowing for live updates rather than complete rebuilds #7
|
|
@ -6,6 +6,9 @@
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
<link rel="manifest" href="manifest.json" />
|
<link rel="manifest" href="manifest.json" />
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-sha256/0.11.0/sha256.min.js"></script>
|
||||||
|
<!-- above allows use of sha256() on http -->
|
||||||
|
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body id="test-body">
|
<body id="test-body">
|
||||||
<!--Cookie Popup(does it matter if im not tracking them? i have no idea)-->
|
<!--Cookie Popup(does it matter if im not tracking them? i have no idea)-->
|
||||||
|
|
@ -101,7 +104,7 @@ changes visibility with JS-->
|
||||||
<input type="checkbox" title="playpausecheck" id="playpausesettingcheckbox"><label for="playpausesettingcheckbox">Play/pause</label><br>
|
<input type="checkbox" title="playpausecheck" id="playpausesettingcheckbox"><label for="playpausesettingcheckbox">Play/pause</label><br>
|
||||||
<input type="checkbox" title="partymodecheck" id="partymodesettingcheckbox"><label for="partymodesettingcheckbox">Toggle Party Mode</label><br>
|
<input type="checkbox" title="partymodecheck" id="partymodesettingcheckbox"><label for="partymodesettingcheckbox">Toggle Party Mode</label><br>
|
||||||
<input type="checkbox" title="volumechangecheck" id="volumechangesettingcheckbox"><label for="volumechangesettingcheckbox">Change volume</label><br>
|
<input type="checkbox" title="volumechangecheck" id="volumechangesettingcheckbox"><label for="volumechangesettingcheckbox">Change volume</label><br>
|
||||||
<input type="checkbox" title="duplicateallowcheck" id="duplicateallowesettingcheckbox"><label for="duplicateallowsettingcheck">Allow Duplicates</label><br>
|
<input type="checkbox" title="duplicateallowcheck" id="duplicateallowesettingcheckbox"><label for="duplicateallowsettingcheck">Add duplicate songs</label><br>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,16 @@
|
||||||
let ip;
|
let ip;
|
||||||
let alertTime = 2;
|
let alertTime = 2;
|
||||||
let adminPass = "";
|
let adminPass = "";
|
||||||
|
let justSkipped = false;
|
||||||
|
let justChangedSetting = false;
|
||||||
const ERR_NO_ADMIN = 401;
|
const ERR_NO_ADMIN = 401;
|
||||||
const VALID_FILE_EXT = ["mp3","flac","wav"];
|
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);
|
const params = new URLSearchParams(location.search);
|
||||||
|
|
||||||
let darkmodetemp = getCookie("darkmode");
|
let darkmodetemp = getCookie("darkmode");
|
||||||
|
|
@ -35,13 +42,19 @@ async function alertText(text="Song Added!") {
|
||||||
}
|
}
|
||||||
// a lot of this is kinda waffly because i was trying to get
|
// a lot of this is kinda waffly because i was trying to get
|
||||||
// it to return the right stuff and javascript is asyrcronouse (boo)
|
// it to return the right stuff and javascript is asyrcronouse (boo)
|
||||||
async function getFromServer(bodyInfo, source="",password=adminPass) {
|
async function getFromServer(bodyInfo, source="", secure=false, password=adminPass) {
|
||||||
try{
|
try{
|
||||||
if (bodyInfo != null) {
|
if (bodyInfo != null) {
|
||||||
// the currently set password is always included in every request
|
// the currently set password is always included in every request
|
||||||
bodyInfo["password"] = password;
|
bodyInfo["password"] = password;
|
||||||
}
|
}
|
||||||
const response = await fetch("http://"+ip+"/"+source, {
|
let href = "";
|
||||||
|
if(secure) {
|
||||||
|
href = "https://"+ip+"/" + source;
|
||||||
|
} else {
|
||||||
|
href = "http://"+ip+"/" + source;
|
||||||
|
}
|
||||||
|
const response = await fetch(href, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(bodyInfo),
|
body: JSON.stringify(bodyInfo),
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -54,9 +67,9 @@ async function getFromServer(bodyInfo, source="",password=adminPass) {
|
||||||
// im suprised i didn't comment on this already but this is kinda lame desing
|
// im suprised i didn't comment on this already but this is kinda lame desing
|
||||||
// its not wrong but you know
|
// its not wrong but you know
|
||||||
// it is easy which i like
|
// it is easy which i like
|
||||||
// and it overrides any other non-async alerts which is nice
|
|
||||||
alertText("Error: Admin restricted action")
|
alertText("Error: Admin restricted action")
|
||||||
} else if(!response.ok){
|
} else if(!response.ok){
|
||||||
|
throw new Error(data.error);
|
||||||
alertText("Error: "+data.error);
|
alertText("Error: "+data.error);
|
||||||
}
|
}
|
||||||
// we add some information from the response just in case it is needed
|
// we add some information from the response just in case it is needed
|
||||||
|
|
@ -67,7 +80,7 @@ async function getFromServer(bodyInfo, source="",password=adminPass) {
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
// console.log("error print here:");
|
// console.log("error print here:");
|
||||||
// console.log(e);
|
// console.log(e);
|
||||||
if (e.toString().contains("TypeError: Failed to fetch")){
|
if (e.toString().includes("TypeError: Failed to fetch")){
|
||||||
alertText("Error: Can't Connect to Server (is the ip set?)")
|
alertText("Error: Can't Connect to Server (is the ip set?)")
|
||||||
} else {
|
} else {
|
||||||
alertText("Error: " + e);
|
alertText("Error: " + e);
|
||||||
|
|
@ -98,15 +111,22 @@ function getCookie(cname) {
|
||||||
// also someone who likes things not being dumb more than me would have separated the client and server buttons
|
// also someone who likes things not being dumb more than me would have separated the client and server buttons
|
||||||
|
|
|||||||
async function controlButton(buttonType) {
|
async function controlButton(buttonType) {
|
||||||
if (buttonType == "pp") { // Play-Pause button
|
if (buttonType == "pp") { // Play-Pause button
|
||||||
|
In the non-OK response branch,
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
```
|
|||||||
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
|
} else if (buttonType == "sk") { // Skip button
|
||||||
let returnCode = getFromServer({control: "skip"}, "controls");
|
// clearInterval(playlistTimeTimer);
|
||||||
|
let returnCode = await getFromServer({control: "skip"}, "controls");
|
||||||
|
// console.log(returnCode["ok"])
|
||||||
if(returnCode["ok"]) {
|
if(returnCode["ok"]) {
|
||||||
if (document.getElementById("playlist-mode").style.display == "block") {
|
if (document.getElementById("playlist-mode").style.display == "block") {
|
||||||
generateVisualPlaylist("skip-button");
|
skipInPlaylist();
|
||||||
|
playlistElapsedSeconds = 0;
|
||||||
|
justSkipped = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (buttonType == "pl") { // Playlist button
|
} else if (buttonType == "pl") { // Playlist button
|
||||||
|
clearInterval(playlistTimeTimer);
|
||||||
document.getElementById("songlist").innerHTML = "";
|
document.getElementById("songlist").innerHTML = "";
|
||||||
document.getElementById("playlist").innerHTML = "<h1 id=\"playlist-alert\"></h1>";
|
document.getElementById("playlist").innerHTML = "<h1 id=\"playlist-alert\"></h1>";
|
||||||
document.getElementById("playlist-mode").style.display = "block";
|
document.getElementById("playlist-mode").style.display = "block";
|
||||||
|
|
@ -114,21 +134,30 @@ async function controlButton(buttonType) {
|
||||||
document.getElementById("settings-mode").style.display = "none";
|
document.getElementById("settings-mode").style.display = "none";
|
||||||
generateVisualPlaylist();
|
generateVisualPlaylist();
|
||||||
} else if (buttonType == "se") { //SearchMode button
|
} else if (buttonType == "se") { //SearchMode button
|
||||||
|
clearInterval(playlistTimeTimer);
|
||||||
document.getElementById("songlist").innerHTML = "<h1>Search to find songs!</h1>";
|
document.getElementById("songlist").innerHTML = "<h1>Search to find songs!</h1>";
|
||||||
document.getElementById("playlist").innerHTML = "";
|
document.getElementById("playlist").innerHTML = "";
|
||||||
document.getElementById("playlist-mode").style.display = "none";
|
document.getElementById("playlist-mode").style.display = "none";
|
||||||
document.getElementById("songlist-mode").style.display = "block";
|
document.getElementById("songlist-mode").style.display = "block";
|
||||||
document.getElementById("settings-mode").style.display = "none";
|
document.getElementById("settings-mode").style.display = "none";
|
||||||
} else if (buttonType == "st") { //Settings button
|
} else if (buttonType == "st") { //Settings button
|
||||||
|
clearInterval(playlistTimeTimer);
|
||||||
document.getElementById("songlist").innerHTML = "";
|
document.getElementById("songlist").innerHTML = "";
|
||||||
document.getElementById("playlist").innerHTML = "";
|
document.getElementById("playlist").innerHTML = "";
|
||||||
document.getElementById("playlist-mode").style.display = "none";
|
document.getElementById("playlist-mode").style.display = "none";
|
||||||
document.getElementById("songlist-mode").style.display = "none";
|
document.getElementById("songlist-mode").style.display = "none";
|
||||||
document.getElementById("settings-mode").style.display = "block";
|
document.getElementById("settings-mode").style.display = "block";
|
||||||
checkSettings()
|
checkSettings()
|
||||||
} else if (buttonType = "pm") { //Partymode toggle (in settings)
|
} else if (buttonType == "pm") { //Partymode toggle (in settings)
|
||||||
await getFromServer({setting: "partymode-toggle"}, "settings")
|
let response = await getFromServer({setting: "partymode-toggle"}, "settings")
|
||||||
checkSettings(true)
|
if(response.ok) {
|
||||||
|
justChangedSetting = true;
|
||||||
|
checkSettings();
|
||||||
|
} else {
|
||||||
|
// dont think anything is needed here
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alertText("Error: You pushed a button that does not exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -245,6 +274,47 @@ function qrCodeGenerate() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSingleSetting(data) {
|
||||||
|
let toBeChanged = data["settingToChange"];
|
||||||
|
if (toBeChanged === "partymode") {
|
||||||
|
document.getElementById("partymode-button").textContent = data["newData"];
|
||||||
|
} else if (toBeChanged === "perms") {
|
||||||
|
let currentAdminPerms = data["newData"];
|
||||||
|
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"];
|
||||||
|
document.getElementById("duplicateallowesettingcheckbox").checked = currentAdminPerms["DUP"];
|
||||||
|
} else if (toBeChanged === "volume") {
|
||||||
|
document.getElementById("volumerange").value = data["newData"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function checkSettings(skipServer=false) {
|
async function checkSettings(skipServer=false) {
|
||||||
//check client stuff first so if the server doesn't exist it can still be changed and seen
|
//check client stuff first so if the server doesn't exist it can still be changed and seen
|
||||||
if (ip.slice(-5)=="19054") {
|
if (ip.slice(-5)=="19054") {
|
||||||
|
|
@ -289,19 +359,91 @@ async function checkSettings(skipServer=false) {
|
||||||
document.getElementById("duplicateallowesettingcheckbox").checked = currentAdminPerms["DUP"];
|
document.getElementById("duplicateallowesettingcheckbox").checked = currentAdminPerms["DUP"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addToPlaylist(songObject) {
|
||||||
|
i = document.getElementById("playlist").children.length-1
|
||||||
|
let newItem = document.createElement("div");
|
||||||
|
newItem.className = "item";
|
||||||
|
newItem.id = Object.keys(songObject)[0];
|
||||||
|
newItem.tabIndex = 0;
|
||||||
|
let image = document.createElement("img");
|
||||||
|
try {
|
||||||
|
if (songObject[newItem.id]["art"] == null) {
|
||||||
|
throw "no image lolz"
|
||||||
|
}
|
||||||
|
image.src = songObject[newItem.id]["art"];
|
||||||
|
} catch(err){
|
||||||
|
image.src = "./images/placeholder.png";
|
||||||
|
}
|
||||||
|
image.id = String(songObject[newItem.id])+" image";
|
||||||
|
let head3 = document.createElement("h3");
|
||||||
|
head3.innerText = songObject[newItem.id]["title"];
|
||||||
|
let head4 = document.createElement("h4");
|
||||||
|
head4.innerText= songObject[newItem.id]["artist"];
|
||||||
|
let head5 = document.createElement("h5");
|
||||||
|
let timeLeft =document.createElement("h5");
|
||||||
|
timeLeft.style.fontWeight = 100;
|
||||||
|
if(i==0) {
|
||||||
|
// they can all have the text, doesn't really matter, but only the first one
|
||||||
|
// should get the ids since its the one we want to mess with
|
||||||
|
head5.id = "playing-indicator-text";
|
||||||
|
timeLeft.id = "elapsed-time-display";
|
||||||
|
}
|
||||||
|
let textdiv = document.createElement("div")
|
||||||
|
textdiv.className="text"
|
||||||
|
newItem.appendChild(image);
|
||||||
|
textdiv.appendChild(head3);
|
||||||
|
textdiv.appendChild(head4);
|
||||||
|
textdiv.appendChild(timeLeft);
|
||||||
|
textdiv.appendChild(head5);
|
||||||
|
newItem.appendChild(textdiv);
|
||||||
|
document.getElementById("playlist").appendChild(newItem);
|
||||||
|
try {
|
||||||
|
if (i == 0) { // Only the first song in the loop gets a time
|
||||||
|
head5.innerHTML="Playing";
|
||||||
|
playlistElapsedSeconds = playlist[0]["time"];
|
||||||
|
playlistSongLength = playlist[0]["length"];
|
||||||
|
displayElapsedPlaylistTime(playlistElapsedSeconds,playlistSongLength);
|
||||||
|
clearInterval(playlistTimeTimer);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.log("I dunno something bad happened:"+e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function skipInPlaylist() {
|
||||||
|
playlistElapsedSeconds = 0;
|
||||||
|
let playlistChildren = document.getElementById("playlist").children;
|
||||||
|
if(playlistChildren[1].nodeName === "DIV") {
|
||||||
|
playlistChildren[1].remove();
|
||||||
|
}
|
||||||
|
playlistChildren = document.getElementById("playlist").children;
|
||||||
|
if(playlistChildren.length === 1) {
|
||||||
|
playlistChildren[0].innerText = "Nothing's Queued..."
|
||||||
|
} else {
|
||||||
|
let firstElementTextChildren = playlistChildren[1].children[1].children
|
||||||
|
// console.log(firstElementTextChildren);
|
||||||
|
firstElementTextChildren[2].id = "elapsed-time-display";
|
||||||
|
firstElementTextChildren[3].id = "playing-indicator-text";
|
||||||
|
firstElementTextChildren[3].textContent = "Playing";
|
||||||
|
}
|
||||||
|
displayElapsedPlaylistTime(playlistElapsedSeconds,playlistSongLength);
|
||||||
|
}
|
||||||
|
|
||||||
async function generateVisualPlaylist(conditions="") {
|
async function generateVisualPlaylist(conditions="") {
|
||||||
document.getElementById("playlist").innerHTML = "<h1 id=\"playlist-alert\"></h1>";
|
document.getElementById("playlist").innerHTML = "<h1 id=\"playlist-alert\"></h1>";
|
||||||
data = await getFromServer(null, "playlist");
|
data = await getFromServer(null, "playlist");
|
||||||
playlist = data["data"];
|
playlist = data["data"]["playlist"];
|
||||||
|
currentlyPlaying = data["data"]["playingState"]
|
||||||
playlist = Object.values(playlist).map(obj => {
|
playlist = Object.values(playlist).map(obj => {
|
||||||
const filename = Object.keys(obj)[0]; // Get the filename
|
const filename = Object.keys(obj)[0]; // Get the filename
|
||||||
const songData = obj[filename]; // Get the song metadata
|
const songData = obj[filename]; // Get the song metadata
|
||||||
return { filename, ...songData }; // Merge filename with song data
|
return { filename, ...songData }; // Merge filename with song data
|
||||||
});
|
});
|
||||||
if (playlist.length==0){
|
if (playlist.length==0){
|
||||||
|
clearInterval(playlistTimeTimer);
|
||||||
document.getElementById("playlist-alert").innerHTML = "Nothing's Queued..."
|
document.getElementById("playlist-alert").innerHTML = "Nothing's Queued..."
|
||||||
} else {
|
} else {
|
||||||
if (conditions=="skip-button") {
|
if (conditions==="skip-button") {
|
||||||
playlist.shift()
|
playlist.shift()
|
||||||
if (playlist.length==0){
|
if (playlist.length==0){
|
||||||
document.getElementById("playlist-alert").innerHTML = "Nothing's Queued..."
|
document.getElementById("playlist-alert").innerHTML = "Nothing's Queued..."
|
||||||
|
|
@ -330,20 +472,11 @@ async function generateVisualPlaylist(conditions="") {
|
||||||
let head5 = document.createElement("h5");
|
let head5 = document.createElement("h5");
|
||||||
let timeLeft =document.createElement("h5");
|
let timeLeft =document.createElement("h5");
|
||||||
timeLeft.style.fontWeight = 100;
|
timeLeft.style.fontWeight = 100;
|
||||||
try {
|
if(i== 0) {
|
||||||
if (i == 0) { // Only the first song in the loop gets a time
|
// they can all have the text, doesn't really matter, but only the first one
|
||||||
head5.innerHTML="Playing";
|
// should get the ids since its the one we want to mess with
|
||||||
if ((conditions != "skip-button")) {
|
head5.id = "playing-indicator-text";
|
||||||
let mins = Math.floor(playlist[i]["time"]/60);
|
timeLeft.id = "elapsed-time-display";
|
||||||
let secs = Math.floor(playlist[i]["time"]%60);
|
|
||||||
let durMins = Math.floor(playlist[i]["length"]/60);
|
|
||||||
let durSecs = Math.floor(playlist[i]["length"]%60);
|
|
||||||
timeLeft.innerHTML = mins.toString() +":"+ secs.toLocaleString('en-US', {minimumIntegerDigits: 2,useGrouping: false}) + "/"+ durMins.toString()+":"+durSecs.toLocaleString('en-US', {minimumIntegerDigits: 2,useGrouping: false});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}catch(err){
|
|
||||||
// i dont know why there's a try catch here but i'm leaving it i dont want to break something
|
|
||||||
console.error(err)
|
|
||||||
}
|
}
|
||||||
let textdiv = document.createElement("div")
|
let textdiv = document.createElement("div")
|
||||||
textdiv.className="text"
|
textdiv.className="text"
|
||||||
|
|
@ -354,13 +487,30 @@ async function generateVisualPlaylist(conditions="") {
|
||||||
textdiv.appendChild(head5);
|
textdiv.appendChild(head5);
|
||||||
newItem.appendChild(textdiv);
|
newItem.appendChild(textdiv);
|
||||||
document.getElementById("playlist").appendChild(newItem);
|
document.getElementById("playlist").appendChild(newItem);
|
||||||
|
try {
|
||||||
|
if (i == 0) { // Only the first song in the loop gets a time
|
||||||
|
head5.innerHTML="Playing";
|
||||||
|
if ((conditions != "skip-button")) {
|
||||||
|
playlistElapsedSeconds = playlist[0]["time"];
|
||||||
|
playlistSongLength = playlist[0]["length"];
|
||||||
|
displayElapsedPlaylistTime(playlistElapsedSeconds,playlistSongLength);
|
||||||
|
clearInterval(playlistTimeTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch(err){
|
||||||
|
// i dont know why there's a try catch here but i'm leaving it i dont want to break something
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
playlistTimeTimer = setInterval(() => {
|
||||||
|
displayElapsedPlaylistTime(playlistElapsedSeconds,playlistSongLength);
|
||||||
|
},1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitSong(songid) {
|
async function submitSong(songid) {
|
||||||
let returncode = await getFromServer({song: songid}, "songadd");
|
let returncode = await getFromServer({song: songid}, "songadd");
|
||||||
if(returncode == ERR_NO_ADMIN) {
|
if(returncode["status"] === ERR_NO_ADMIN) {
|
||||||
// right now the error is alerted in getFromServer, maybe will change that
|
// right now the error is alerted in getFromServer, maybe will change that
|
||||||
} else if(returncode["status"]!==200) {
|
} else if(returncode["status"]!==200) {
|
||||||
alertText("That song's already in the queue! Hang on!")
|
alertText("That song's already in the queue! Hang on!")
|
||||||
|
|
@ -398,19 +548,19 @@ function toggleDark(e) {
|
||||||
qrCodeGenerate();
|
qrCodeGenerate();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sha256(message) {
|
// async function sha256(message) {
|
||||||
// Encode the message as UTF-8
|
// // Encode the message as UTF-8
|
||||||
const msgBuffer = new TextEncoder().encode(message);
|
// const msgBuffer = new TextEncoder().encode(message);
|
||||||
|
|
||||||
// Hash the message
|
// // Hash the message
|
||||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
// const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
||||||
|
|
||||||
// Convert ArrayBuffer to hex string
|
// // Convert ArrayBuffer to hex string
|
||||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
// const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
// const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
|
||||||
return hashHex;
|
// return hashHex;
|
||||||
}
|
// }
|
||||||
|
|
||||||
async function adminPassEnter(e) {
|
async function adminPassEnter(e) {
|
||||||
if (e.key == "Enter") {
|
if (e.key == "Enter") {
|
||||||
|
|
@ -440,6 +590,8 @@ async function submitPerms(e) {
|
||||||
// its not perfect if you spam click, but it gets the point across to the user
|
// its not perfect if you spam click, but it gets the point across to the user
|
||||||
let clickedBox = e.srcElement;
|
let clickedBox = e.srcElement;
|
||||||
clickedBox.checked = !clickedBox.checked;
|
clickedBox.checked = !clickedBox.checked;
|
||||||
|
} else {
|
||||||
|
justChangedSetting = true;
|
||||||
|
`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);
}
```
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -464,10 +616,10 @@ document.addEventListener('keydown', function(e){
|
||||||
}})
|
}})
|
||||||
document.getElementById("playlist-mode").style.display = "none";
|
document.getElementById("playlist-mode").style.display = "none";
|
||||||
document.getElementById("settings-mode").style.display = "none";
|
document.getElementById("settings-mode").style.display = "none";
|
||||||
document.getElementById("volumerange").onchange = async function() {
|
document.getElementById("volumerange").onchange = async function(e) {
|
||||||
// there is no reason for this not to be a defined function
|
// there is no reason for this not to be a defined function
|
||||||
// FIX THIS
|
// FIX THIS
|
||||||
let returnValue = await getFromServer({setting:"volume",level:this.value}, "settings")
|
let returnValue = await getFromServer({setting:"volume",level:e.target.value}, "settings")
|
||||||
if (returnValue["status"] == ERR_NO_ADMIN) {
|
if (returnValue["status"] == ERR_NO_ADMIN) {
|
||||||
// alertText("Error: Admin restricted action");
|
// alertText("Error: Admin restricted action");
|
||||||
// there's an admin restrict alert built into getFromServer
|
// there's an admin restrict alert built into getFromServer
|
||||||
|
|
@ -508,8 +660,9 @@ document.getElementById("songlist").addEventListener('keydown', function(e){chec
|
||||||
document.getElementById("songlist").addEventListener('click', function(e){checkWhatSongWasClicked(e)});
|
document.getElementById("songlist").addEventListener('click', function(e){checkWhatSongWasClicked(e)});
|
||||||
|
|
||||||
//makes the controls look mostly normal on all screens, best solution i could find, idk man
|
//makes the controls look mostly normal on all screens, best solution i could find, idk man
|
||||||
let tempWidth = document.getElementById('controls').clientWidth;
|
// replaced this with "transform" css stuff
|
||||||
document.getElementById("controls").style.marginLeft = "-"+String(parseInt(tempWidth/2))+"px";
|
// let tempWidth = document.getElementById('controls').clientWidth;
|
||||||
|
// document.getElementById("controls").style.marginLeft = "-"+String(parseInt(tempWidth/2))+"px";
|
||||||
|
|
||||||
//for my use case (my immediate family), they dont know how to set an ip
|
//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
|
//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
|
||||||
|
|
@ -536,3 +689,42 @@ if (alertTime == "") {
|
||||||
}
|
}
|
||||||
// this is the code that makes the qr code at the very start
|
// this is the code that makes the qr code at the very start
|
||||||
qrCodeGenerate()
|
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);
|
||||||
|
addToPlaylist(data);
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("timeUpdate", function(data) {
|
||||||
|
// console.log("recieved data from timeUpdate");
|
||||||
|
// console.log(data);
|
||||||
|
playlistElapsedSeconds = data["elapsedTime"];
|
||||||
|
currentlyPlaying = data["playingState"]
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("skipSong",() => {
|
||||||
|
if(justSkipped === false) {
|
||||||
|
skipInPlaylist();
|
||||||
|
} else {
|
||||||
|
justSkipped = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("settingsChange",(data) => {
|
||||||
|
// console.log(data);
|
||||||
|
if(justChangedSetting) {
|
||||||
|
// console.log("working");
|
||||||
|
justChangedSetting = false;
|
||||||
|
} else {
|
||||||
|
// checkSettings();
|
||||||
|
updateSingleSetting(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -62,6 +62,7 @@ h4 {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
transform: translateX(-50%);
|
||||||
background-color:var(--bg-main);
|
background-color:var(--bg-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
|
from flask_socketio import SocketIO
|
||||||
import sqlite3 as sql
|
import sqlite3 as sql
|
||||||
import vlc,threading,time,random,argparse,dotenv,os,hashlib,string
|
import vlc,threading,time,random,argparse,dotenv,os,hashlib,string
|
||||||
# Argparse Stuff
|
# Argparse Stuff
|
||||||
|
|
@ -64,18 +65,45 @@ player.audio_set_volume(100)
|
||||||
app = Flask(__name__)
|
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)
|
CORS(app)
|
||||||
|
# Replace the star with the frontend domain if you dislike being hacked
|
||||||
|
socketio = SocketIO(app, cors_allowed_origins="*")
|
||||||
|
|
||||||
def queueSong(song):
|
def queueSong(song):
|
||||||
with playlistLock:
|
with playlistLock:
|
||||||
playlist.append(song)
|
playlist.append(song)
|
||||||
|
socketio.emit("songAdd",getSongInfo(song))
|
||||||
|
|
||||||
|
def getSongInfo(song):
|
||||||
|
fileofDB = sql.connect("songDatabase.db")
|
||||||
|
`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.
|
# this is a loop that plays the songs and checks for playlist changes, skips, ect.
|
||||||
|
counter = 0
|
||||||
|
isPlaying = False
|
||||||
def playQueuedSongs():
|
def playQueuedSongs():
|
||||||
global skipNow
|
global skipNow
|
||||||
global songNext
|
global songNext
|
||||||
global partyMode
|
global partyMode
|
||||||
|
global counter
|
||||||
|
global isPlaying
|
||||||
while True:
|
while True:
|
||||||
with playlistLock:
|
with playlistLock:
|
||||||
|
counter+=1
|
||||||
|
if(counter > 2):
|
||||||
|
playingState = str(player.get_state()) == "State.Playing"
|
||||||
|
socketio.emit('timeUpdate',{"elapsedTime":player.get_time()/1000,"playingState":playingState})
|
||||||
|
counter = 0
|
||||||
playerState = str(player.get_state())
|
playerState = str(player.get_state())
|
||||||
endStates = ["State.Ended","State.Stopped","State.NothingSpecial"]
|
endStates = ["State.Ended","State.Stopped","State.NothingSpecial"]
|
||||||
if playlist and (playerState in endStates or skipNow == True):
|
if playlist and (playerState in endStates or skipNow == True):
|
||||||
|
|
@ -86,7 +114,13 @@ def playQueuedSongs():
|
||||||
media = vlcInstance.media_new(soundLocation+songNext)
|
media = vlcInstance.media_new(soundLocation+songNext)
|
||||||
player.set_media(media)
|
player.set_media(media)
|
||||||
player.play()
|
player.play()
|
||||||
|
isPlaying = True
|
||||||
|
socketio.emit("skipSong",None)
|
||||||
elif (skipNow==True or (playerState in endStates)):
|
elif (skipNow==True or (playerState in endStates)):
|
||||||
|
if(isPlaying):
|
||||||
|
socketio.emit("skipSong",None)
|
||||||
|
isPlaying = False
|
||||||
|
# print(playerState)
|
||||||
# skip was pressed and there are no new songs
|
# skip was pressed and there are no new songs
|
||||||
skipNow=False
|
skipNow=False
|
||||||
songNext = None
|
songNext = None
|
||||||
|
|
@ -99,11 +133,16 @@ def playQueuedSongs():
|
||||||
# adds the random songs for party mode
|
# 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)
|
# the above 2 means this only applies if (a song is playing or paused) and (the queue is empty)
|
||||||
playlist.append(result[0][0])
|
playlist.append(result[0][0])
|
||||||
|
socketio.emit('songAdd',getSongInfo(result[0][0]))
|
||||||
# check for new songs every second
|
# 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
|
# 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)
|
time.sleep(1)
|
||||||
|
|
||||||
|
@socketio.on("connect")
|
||||||
|
def handleConnect():
|
||||||
|
pass
|
||||||
|
|
||||||
@app.route("/controls", methods=['POST'])
|
@app.route("/controls", methods=['POST'])
|
||||||
def playerControls():
|
def playerControls():
|
||||||
# recieve control inputs (play/pause and skip) from the webUI
|
# recieve control inputs (play/pause and skip) from the webUI
|
||||||
|
|
@ -113,10 +152,12 @@ def playerControls():
|
||||||
try:
|
try:
|
||||||
if recieveData["control"] == "play-pause":
|
if recieveData["control"] == "play-pause":
|
||||||
if ADMIN_PASS == recieveData['password'] or controlPerms["PP"]:
|
if ADMIN_PASS == recieveData['password'] or controlPerms["PP"]:
|
||||||
|
playingState = str(player.get_state())=="State.Playing"
|
||||||
player.pause()
|
player.pause()
|
||||||
return ERR_200
|
return {"error":"ok","data":{"playingState":not(playingState)}},200
|
||||||
else:
|
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":
|
elif recieveData["control"] == "skip":
|
||||||
if ADMIN_PASS == recieveData['password'] or controlPerms["SK"]:
|
if ADMIN_PASS == recieveData['password'] or controlPerms["SK"]:
|
||||||
skipNow = True
|
skipNow = True
|
||||||
|
|
@ -149,6 +190,9 @@ def settingsControl():
|
||||||
volumeLevel = int(recieveData["level"])
|
volumeLevel = int(recieveData["level"])
|
||||||
if(volumeLevel <= 100 and volumeLevel >= 0):
|
if(volumeLevel <= 100 and volumeLevel >= 0):
|
||||||
volumePassed = player.audio_set_volume(volumeLevel)
|
volumePassed = player.audio_set_volume(volumeLevel)
|
||||||
|
if(volumePassed == 0):
|
||||||
|
# only emit a signal i the volume really changed
|
||||||
|
socketio.emit("settingsChange",{"settingToChange":"volume","newData":volumeLevel})
|
||||||
return {"error":"ok","data":{"volumePassed":volumePassed}},200
|
return {"error":"ok","data":{"volumePassed":volumePassed}},200
|
||||||
else:
|
else:
|
||||||
return {"error":"Invalid volume level","data":None},422
|
return {"error":"Invalid volume level","data":None},422
|
||||||
|
|
@ -157,13 +201,16 @@ def settingsControl():
|
||||||
elif recieveData["setting"] == "partymode-toggle":
|
elif recieveData["setting"] == "partymode-toggle":
|
||||||
if ADMIN_PASS == recieveData['password'] or controlPerms["PM"]:
|
if ADMIN_PASS == recieveData['password'] or controlPerms["PM"]:
|
||||||
partyMode = not(partyMode)
|
partyMode = not(partyMode)
|
||||||
|
partyModeStr = "On" if partyMode else "Off"
|
||||||
|
socketio.emit("settingsChange",{"settingToChange":"partymode","newData":partyModeStr})
|
||||||
return ERR_200
|
return ERR_200
|
||||||
else:
|
else:
|
||||||
return ERR_NO_ADMIN
|
return ERR_NO_ADMIN
|
||||||
elif recieveData["setting"] == "perms":
|
elif recieveData["setting"] == "perms":
|
||||||
if ADMIN_PASS == recieveData["password"]:
|
if ADMIN_PASS == recieveData["password"]:
|
||||||
controlPerms = recieveData["admin"]
|
controlPerms = recieveData["admin"]
|
||||||
print(recieveData["admin"])
|
# print(recieveData["admin"])
|
||||||
|
socketio.emit("settingsChange",{"settingToChange":"perms","newData":controlPerms})
|
||||||
return ERR_200
|
return ERR_200
|
||||||
else:
|
else:
|
||||||
return ERR_NO_ADMIN
|
return ERR_NO_ADMIN
|
||||||
|
|
@ -261,8 +308,11 @@ def getPlaylist():
|
||||||
}
|
}
|
||||||
tempPlaylist.append({i:k})
|
tempPlaylist.append({i:k})
|
||||||
fileofDB.close()
|
fileofDB.close()
|
||||||
|
playingState = False
|
||||||
return {"error":"ok","data":tempPlaylist}
|
if(str(player.get_state())=="State.Playing"):
|
||||||
|
playingState = True
|
||||||
|
# print(playingState)
|
||||||
|
return {"error":"ok","data":{"playlist":tempPlaylist,"playingState":playingState}},200
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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
|
# 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 +321,5 @@ if __name__ == "__main__":
|
||||||
queueThread = threading.Thread(target=playQueuedSongs)
|
queueThread = threading.Thread(target=playQueuedSongs)
|
||||||
queueThread.daemon = True
|
queueThread.daemon = True
|
||||||
queueThread.start()
|
queueThread.start()
|
||||||
app.run(host='0.0.0.0', port=portTheUserPicked)
|
socketio.run(app=app,host='0.0.0.0', port=portTheUserPicked)
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ The client is a web application that can be hosted on any server, it need not be
|
||||||
### Server Setup:
|
### Server Setup:
|
||||||
**Pre-setup:** If you want the songs to have art associated with them, it is all hosted on and retrieved from LastFM, and you will need to sign up for a developer app, and put your key in the database generator \
|
**Pre-setup:** If you want the songs to have art associated with them, it is all hosted on and retrieved from LastFM, and you will need to sign up for a developer app, and put your key in the database generator \
|
||||||
\
|
\
|
||||||
The server side consists of 3 files:
|
The server side consists of 3 files and a directory:
|
||||||
|
|
||||||
```
|
```
|
||||||
sound/
|
sound/
|
||||||
|
|
@ -60,7 +60,6 @@ These are specific details on each section of the app, and how to use them
|
||||||
- Accepts Play-Pause and Skip commands
|
- Accepts Play-Pause and Skip commands
|
||||||
- Uses port 19054 by default
|
- Uses port 19054 by default
|
||||||
- Can be changed in the `.env` file
|
- Can be changed in the `.env` file
|
||||||
- The default port can be changed in the file
|
|
||||||
- Running with `--admin (admin password)` sets an admin password for moderation on the client
|
- Running with `--admin (admin password)` sets an admin password for moderation on the client
|
||||||
- ***Note: Do not reuse a password, the password is hashed before being sent over the network, but I still wouldn't bet my house on it, no security is guaranteed***
|
- ***Note: Do not reuse a password, the password is hashed before being sent over the network, but I still wouldn't bet my house on it, 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)
|
- 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)
|
||||||
|
|
@ -70,6 +69,9 @@ These are specific details on each section of the app, and how to use them
|
||||||
- Add track to queue
|
- Add track to queue
|
||||||
- Partymode toggle
|
- Partymode toggle
|
||||||
- Change volume
|
- Change volume
|
||||||
|
- Add duplicate track to queue
|
||||||
|
- This is a seperate toggle, but is based on the setting "Add track to queue"
|
||||||
|
- Basically if you can't add at all, you can't add a duplicate either (obviously)
|
||||||
- When this argument is left out a random string is generated and printed in console to be the admin password (so keep the console window hidden if you don't want to use it)
|
- When this argument is left out a random string is generated and printed in console to be the admin password (so keep the console window hidden if you don't want to use it)
|
||||||
|
|
||||||
### Client:
|
### Client:
|
||||||
|
|
@ -89,6 +91,7 @@ From left to right:
|
||||||
- Volume controls the VLC volume of the connected server
|
- Volume controls the VLC volume of the connected server
|
||||||
- *Because the volume can be controlled in the client, for best usage set your device volume as high as possible and turn it down using this slider*
|
- *Because the volume can be controlled in the client, for best usage set your device volume as high as possible and turn it down using this slider*
|
||||||
- QR code to allow others to connect to and use the Remote
|
- QR code to allow others to connect to and use the Remote
|
||||||
|
- Admin password can be set to restrict actions for general users, or avoid the set restrictions
|
||||||
|
|
||||||
### A quick note on the password feature
|
### A quick note on the password feature
|
||||||
|
|
||||||
|
|
|
||||||
BIN
requirements.txt
14
wishlist.md
|
|
@ -1,7 +1,6 @@
|
||||||
## Wishlist
|
## Wishlist
|
||||||
*Features I would like to add, will be completed in any order*
|
*Features I would like to add, will be completed in any order*
|
||||||
- [x] Admin password
|
- [ ] Loading indicator while awaiting server stuff
|
||||||
* Allows restricting certain features and changing permissions on the fly on the client
|
|
||||||
- [ ] Refactoring existing code
|
- [ ] Refactoring existing code
|
||||||
- [x] Remove old comments
|
- [x] Remove old comments
|
||||||
- [ ] Update the SQL -> Server -> Client pipeline when searching and building playlist
|
- [ ] Update the SQL -> Server -> Client pipeline when searching and building playlist
|
||||||
|
|
@ -23,7 +22,10 @@
|
||||||
- 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)
|
- 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
|
- Potentially a "redemption code" system, which can be tracked client side
|
||||||
- All of this is very hackable without a server-side login.
|
- 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
|
- [x] 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
|
- [x] Set a timeout to change the time (to start)
|
||||||
* websockets can re-update clients
|
- [x] Send updates to the playlist in real time when songs are added
|
||||||
* not actually sure if i can CORS-socket but we're sure gonna try
|
- [x] 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)
|
||||||
|
- [x] Settings updates
|
||||||
|
- [x] Without re-posting the server (contain update data in websocket ping)
|
||||||
else if (buttonType = "pm")assigns instead of comparing, so this branch will always execute and also mutatesbuttonType. Change this to a comparison (===) so only the intended button triggers the party mode toggle.