1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
server/sound/
|
server/sound/
|
||||||
server/songDatabase.json
|
server/songDatabase.json
|
||||||
|
start.bat
|
||||||
BIN
Client/favicon.ico
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
Client/images/placeholder.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
Client/images/play-pause.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
Client/images/playlist.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
Client/images/search.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
Client/images/settings.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
Client/images/skip.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
97
Client/index.html
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Jukebox Controller</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<link rel="manifest" href="manifest.json" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!--Cookie Popup(does it matter if im not tracking them? i have no idea)-->
|
||||||
|
<script type="text/javascript" src="https://cookieconsent.popupsmart.com/src/js/popper.js"></script><script> window.start.init({Palette:"palette2",Mode:"floating left",Theme:"classic",LinkText:" Learn More!",Message:"This website uses cookies to save necessary data to your device, and no tracking is performed.",Time:"0",})</script>
|
||||||
|
|
||||||
|
<div class="intro">
|
||||||
|
<h1 id="title">Jukebox Remote</h1>
|
||||||
|
<p>Add songs to the shared playlist below!</p>
|
||||||
|
</div>
|
||||||
|
<h1 class="alert" id="alert"></h1>
|
||||||
|
<!-- this is the main body when PICKING songs to add to the playlist,
|
||||||
|
changes visibility with JS-->
|
||||||
|
<div class="songlist-mode" id="songlist-mode">
|
||||||
|
<div class="searchbox-holder">
|
||||||
|
<input type="text" autocomplete="off" placeholder="Search" id="songsearch" class="searchbox"><button class="go-search" id="go-search">Go!</button>
|
||||||
|
</div>
|
||||||
|
<div class="songlist" id="songlist">
|
||||||
|
<h1>Search to find songs!</h1>
|
||||||
|
<!-- Placeholder for the song items
|
||||||
|
These are generated using javascript for search
|
||||||
|
|
||||||
|
<div class="item">
|
||||||
|
<img src="search.png"></img>
|
||||||
|
<h3><span>Song title</span></h3>
|
||||||
|
<h4>Artist</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- this is the opposite of the thing described above (looking at songs in queue) -->
|
||||||
|
<div class="playlist-mode" id="playlist-mode">
|
||||||
|
<div class="playlist" id="playlist">
|
||||||
|
<h1 id="playlist-alert"></h1>
|
||||||
|
<!-- The template for playlist items
|
||||||
|
<div class="item">
|
||||||
|
<img src="placeholder.png"></img>
|
||||||
|
<h3>Song title</h3>
|
||||||
|
<h4>Artist</h4>
|
||||||
|
<h5>Playing</h5>
|
||||||
|
</div>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Because settings involves no generation ( all the settings are known), it is filled out always, just dissapears-->
|
||||||
|
<div class="settings-mode" id="settings-mode">
|
||||||
|
<div class="settings" id="settings">
|
||||||
|
<h1 >Client Settings (Saved to device)</h1>
|
||||||
|
<!--
|
||||||
|
<div class="item">
|
||||||
|
<h2 for="darkmode-button">Dark Mode:</h2>
|
||||||
|
<p class="italic">opposite of light mode</p>
|
||||||
|
<button title="darkmode-button" id="darkmode-button">Off</button>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<div class="item">
|
||||||
|
<h2 for="iptextbox">Server IP:</h2>
|
||||||
|
<p class="italic">IP of the device running the song server</p>
|
||||||
|
<input title="iptextbox" style="width:200px" type="text" id="iptextbox">
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<h2 for="alerttimetextbox">Alert Time:</h2>
|
||||||
|
<p class="italic">How long alerts stay on screen for (seconds)</p>
|
||||||
|
<input title="alerttimetextbox" style="width:50px" type="text" id="alerttimetextbox" value="2">
|
||||||
|
</div>
|
||||||
|
<h1>Server Settings (Saved to server)</h1>
|
||||||
|
<div class="item">
|
||||||
|
<h2 for="partymode-button">Party Mode:</h2>
|
||||||
|
<p class="italic">Add random songs to the queue when it is about to be empty</p>
|
||||||
|
<button title="partymode-button" id="partymode-button">N/A</button>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<h2 for="volumerange">Volume:</h2>
|
||||||
|
<p class="italic">Volume of the music</p>
|
||||||
|
<input type="range" min="0" max="100" step="1" title="volumerange" id="volumerange">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--All the buttons are down here but settings is just doing its own thing-->
|
||||||
|
<img class="settings-button" id="settings-button" src="./images/settings.png" alt="settings"></img>
|
||||||
|
<div id="controls" class="controls">
|
||||||
|
<img class="control-button" id="playlist-button" src="./images/playlist.png" alt="playlist"></img>
|
||||||
|
<img class="control-button" id="play-pause-button" src="./images/play-pause.png" alt="play pause"></img>
|
||||||
|
<img class="control-button" id="skip-button" src="./images/skip.png" alt="skip"></img>
|
||||||
|
<img class="control-button" id="search-button" src="./images/search.png" alt="search"></img>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script src="scripts.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
Client/manifest.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "Jukebox Remote",
|
||||||
|
"short_name": "Jukebox Remote",
|
||||||
|
"start_url": "index.html",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#eeeeee",
|
||||||
|
"theme_color": "#eeeeee",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/favicon.ico",
|
||||||
|
"type": "image/ico", "sizes": "100x100"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
331
Client/scripts.js
Normal file
|
|
@ -0,0 +1,331 @@
|
||||||
|
let ip
|
||||||
|
let alertTime = 2
|
||||||
|
async function alertText(text="Song Added!") {
|
||||||
|
alertbox = document.getElementById("alert");
|
||||||
|
alertbox.innerHTML = text;
|
||||||
|
await new Promise(r => setTimeout(r, alertTime*1000));
|
||||||
|
if (alertbox.innerHTML == text) {
|
||||||
|
alertbox.innerHTML = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// a lot of this is kinda waffly because i was trying to get
|
||||||
|
// it to return the right stuff and javascript is asyrcronouse (boo)
|
||||||
|
async function getFromServer(bodyInfo, source="") {
|
||||||
|
try{
|
||||||
|
const response = await fetch("http://"+ip+"/"+source, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(bodyInfo),
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json; charset=UTF-8"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
return await data;
|
||||||
|
} catch(e) {
|
||||||
|
if (e == "TypeError: Failed to fetch"){
|
||||||
|
alertText("error: NoConnect to Server (is the ip set?)")
|
||||||
|
} else {
|
||||||
|
alertText("error: " + e)
|
||||||
|
}
|
||||||
|
const response=null;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//cookie reader is taken from internet because cookies ae too complicated for me
|
||||||
|
//i still understand how it works though promise just i see no reason to write this from scratch
|
||||||
|
function getCookie(cname) {
|
||||||
|
let name = cname + "=";
|
||||||
|
let decodedCookie = decodeURIComponent(document.cookie);
|
||||||
|
let ca = decodedCookie.split(';');
|
||||||
|
for(let i = 0; i <ca.length; i++) {
|
||||||
|
let c = ca[i];
|
||||||
|
while (c.charAt(0) == ' ') {
|
||||||
|
c = c.substring(1);
|
||||||
|
}
|
||||||
|
if (c.indexOf(name) == 0) {
|
||||||
|
return c.substring(name.length, c.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
//someone more organised than me would have set all these elements to variables so they dont have to get them 50 times
|
||||||
|
async function controlButton(buttonType) {
|
||||||
|
if (buttonType == "pp") {
|
||||||
|
getFromServer({control: "play-pause"}, "controls")
|
||||||
|
} else if (buttonType == "sk") {
|
||||||
|
getFromServer({control: "skip"}, "controls")
|
||||||
|
if (document.getElementById("playlist-mode").style.display == "block") {
|
||||||
|
generateVisualPlaylist("skip-button");
|
||||||
|
}
|
||||||
|
} else if (buttonType == "pl") {
|
||||||
|
document.getElementById("songlist").innerHTML = "";
|
||||||
|
document.getElementById("playlist").innerHTML = "<h1 id=\"playlist-alert\"></h1>";
|
||||||
|
document.getElementById("playlist-mode").style.display = "block";
|
||||||
|
document.getElementById("songlist-mode").style.display = "none";
|
||||||
|
document.getElementById("settings-mode").style.display = "none";
|
||||||
|
generateVisualPlaylist();
|
||||||
|
} else if (buttonType == "se") {
|
||||||
|
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") {
|
||||||
|
document.getElementById("songlist").innerHTML = "";
|
||||||
|
document.getElementById("playlist").innerHTML = "";
|
||||||
|
document.getElementById("playlist-mode").style.display = "none";
|
||||||
|
document.getElementById("songlist-mode").style.display = "none";
|
||||||
|
document.getElementById("settings-mode").style.display = "block";
|
||||||
|
checkSettings()
|
||||||
|
} else if (buttonType = "pm") {
|
||||||
|
await getFromServer({setting: "partymode-toggle"}, "settings")
|
||||||
|
checkSettings(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchSongsEnter(e) {
|
||||||
|
if (e.keyCode == 13) {
|
||||||
|
searchSongs(document.getElementById("songsearch").value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchSongs(searchTerm){
|
||||||
|
let optionslist = []
|
||||||
|
|
||||||
|
document.getElementById("songlist").innerHTML = ""
|
||||||
|
searchResults = await getFromServer({search:searchTerm},"search").then()
|
||||||
|
for (let index in searchResults) {
|
||||||
|
optionslist.push([index,searchResults[index][0],searchResults[index][1],searchResults[index][2]]);
|
||||||
|
}
|
||||||
|
//generate the visual song list
|
||||||
|
for(let i = 0; i < optionslist.length; i++) {
|
||||||
|
let newItem = document.createElement("div");
|
||||||
|
newItem.className = "item";
|
||||||
|
newItem.id = optionslist[i][3];
|
||||||
|
let image = document.createElement("img");
|
||||||
|
try {
|
||||||
|
if (optionslist[i][2] == null) {
|
||||||
|
throw "no image lolz"
|
||||||
|
}
|
||||||
|
image.src = optionslist[i][2];
|
||||||
|
} catch(err){
|
||||||
|
image.src = "./images/placeholder.png";
|
||||||
|
}
|
||||||
|
image.id = String(optionslist[i][3])+" image";
|
||||||
|
let head3 = document.createElement("h3");
|
||||||
|
head3.innerText = optionslist[i][0];
|
||||||
|
let head4 = document.createElement("h4");
|
||||||
|
head4.innerText=optionslist[i][1];
|
||||||
|
newItem.appendChild(image);
|
||||||
|
newItem.appendChild(head3);
|
||||||
|
newItem.appendChild(head4);
|
||||||
|
document.getElementById("songlist").appendChild(newItem);
|
||||||
|
//display error if no results
|
||||||
|
|
||||||
|
}
|
||||||
|
if (optionslist.length == 0) {
|
||||||
|
document.getElementById("songlist").innerHTML = "<h1>We might not have that one...</h1>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function alertTimeEnter(e){
|
||||||
|
if (e.key == "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
alertTimeSet(document.getElementById("alerttimetextbox").value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function alertTimeSet(time) {
|
||||||
|
alertTime = time;
|
||||||
|
document.cookie = "alertTime="+alertTime+"; path=/;"
|
||||||
|
alertText("Alerts stay on screen for " + alertTime.toString() + " seconds")
|
||||||
|
}
|
||||||
|
|
||||||
|
function ipSetEnter(e){
|
||||||
|
if (e.key == "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
ipSetter(document.getElementById("iptextbox").value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ipSetter(){
|
||||||
|
ipBox = document.getElementById("iptextbox").value
|
||||||
|
if (ipBox == "") {
|
||||||
|
alertText("Your IP is set to "+ip)
|
||||||
|
} else {
|
||||||
|
if (ipBox.includes(":")) {
|
||||||
|
port = ipBox.slice(ipBox.indexOf(":")+1)
|
||||||
|
ip = ipBox;
|
||||||
|
document.cookie = "ip="+ip+"; path=/;"
|
||||||
|
alertText("Your IP is now set to "+ip.slice(0, ipBox.indexOf(":"))+" at port "+port)
|
||||||
|
} else {
|
||||||
|
ip = ipBox + ":19054"
|
||||||
|
document.cookie = "ip="+ip+"; path=/;"
|
||||||
|
alertText("Your IP is now set to "+ipBox+" at port 19054 (Default)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkSettings(skipServer=false) {
|
||||||
|
//check client stuff first so if the server doesn't exist it can still be changed and seen
|
||||||
|
if (ip.slice(-5)=="19054") {
|
||||||
|
document.getElementById("iptextbox").value = ip.slice(0,-6)
|
||||||
|
} else {
|
||||||
|
document.getElementById("iptextbox").value = ip;
|
||||||
|
}
|
||||||
|
document.getElementById("alerttimetextbox").value = alertTime
|
||||||
|
partyButtonState = document.getElementById("partymode-button").innerHTML;
|
||||||
|
x = await getFromServer({setting: "getsettings"}, "settings");
|
||||||
|
if (!(skipServer) || partyButtonState=="N/A") {
|
||||||
|
if (x["partymode"] == false) {
|
||||||
|
document.getElementById("partymode-button").innerHTML = "Off";
|
||||||
|
} else {
|
||||||
|
document.getElementById("partymode-button").innerHTML = "On";
|
||||||
|
}
|
||||||
|
} else if (document.getElementById("partymode-button").innerHTML == "Off") {
|
||||||
|
document.getElementById("partymode-button").innerHTML = "On";
|
||||||
|
} else {
|
||||||
|
document.getElementById("partymode-button").innerHTML = "Off";
|
||||||
|
}
|
||||||
|
document.getElementById("volumerange").value = parseInt(x["volume"])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateVisualPlaylist(conditions="") {
|
||||||
|
document.getElementById("playlist").innerHTML = "<h1 id=\"playlist-alert\"></h1>";
|
||||||
|
playlist = await getFromServer(null, "playlist");
|
||||||
|
if (playlist.length==0){
|
||||||
|
document.getElementById("playlist-alert").innerHTML = "Nothing's Queued..."
|
||||||
|
} else {
|
||||||
|
if (conditions=="skip-button") {
|
||||||
|
playlist.shift()
|
||||||
|
if (playlist.length==0){
|
||||||
|
document.getElementById("playlist-alert").innerHTML = "Nothing's Queued..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i in playlist) {
|
||||||
|
let newItem = document.createElement("div");
|
||||||
|
newItem.className = "item";
|
||||||
|
newItem.id = playlist[i]["file"];
|
||||||
|
let image = document.createElement("img");
|
||||||
|
try {
|
||||||
|
if (playlist[i]["art"] == null) {
|
||||||
|
throw "no image lolz"
|
||||||
|
}
|
||||||
|
image.src = playlist[i]["art"];
|
||||||
|
} catch(err){
|
||||||
|
image.src = "./images/placeholder.png";
|
||||||
|
}
|
||||||
|
image.id = String(playlist[i]["file"])+" image";
|
||||||
|
let head3 = document.createElement("h3");
|
||||||
|
head3.innerText = playlist[i]["title"];
|
||||||
|
let head4 = document.createElement("h4");
|
||||||
|
head4.innerText=playlist[i]["artist"];
|
||||||
|
let head5 = document.createElement("h5");
|
||||||
|
let timeLeft =document.createElement("h5");
|
||||||
|
timeLeft.style.fontWeight = 100;
|
||||||
|
try {
|
||||||
|
if (i == 0) {
|
||||||
|
head5.innerHTML="Playing";
|
||||||
|
if ((conditions != "skip-button")) {
|
||||||
|
let mins = Math.floor(playlist[i]["time"]/60);
|
||||||
|
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){
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitSong(songid) {
|
||||||
|
getFromServer({song: songid}, "songadd")
|
||||||
|
alertText("Added to Queue")
|
||||||
|
}
|
||||||
|
function checkWhatSongWasClicked(e) {
|
||||||
|
itemId = e.srcElement.id;
|
||||||
|
if ((itemId.length-itemId.lastIndexOf("image") == 5) && itemId.lastIndexOf("image")!=-1) {
|
||||||
|
itemId = itemId.slice(0,-6)
|
||||||
|
}
|
||||||
|
//i feel like later dylan won't apreciate this
|
||||||
|
//one of my files was "file.MP3" so it didn't work
|
||||||
|
if (itemId.slice(-4).toLowerCase() == ".mp3") {
|
||||||
|
submitSong(itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let optionslist = []
|
||||||
|
|
||||||
|
//sets all de stuff for buttons
|
||||||
|
document.addEventListener('keydown', function(e){
|
||||||
|
if (e.key == "/"){
|
||||||
|
document.getElementById("title").scrollIntoView();
|
||||||
|
document.getElementById("songsearch").select();
|
||||||
|
e.preventDefault()
|
||||||
|
}})
|
||||||
|
document.getElementById("playlist-mode").style.display = "none";
|
||||||
|
document.getElementById("settings-mode").style.display = "none";
|
||||||
|
//.ontouch for mobile??
|
||||||
|
document.getElementById("volumerange").onchange = function() {
|
||||||
|
getFromServer({setting:"volume",level:this.value}, "settings")
|
||||||
|
if (this.value == 0) {
|
||||||
|
alertText("The volume is now set to 0 (Pause?)")
|
||||||
|
} else {
|
||||||
|
alertText("The volume is now set to " + this.value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
document.getElementById("settings-button").addEventListener('click',function(){controlButton("st")});
|
||||||
|
document.getElementById("play-pause-button").addEventListener('click', function(){controlButton("pp")});
|
||||||
|
document.getElementById("playlist-button").addEventListener('click', function(){controlButton("pl")});
|
||||||
|
document.getElementById("search-button").addEventListener('click', function(){controlButton("se")});
|
||||||
|
document.getElementById("skip-button").addEventListener('click',function(){controlButton("sk")});
|
||||||
|
document.getElementById("go-search").addEventListener('click', function(){searchSongs(document.getElementById("songsearch").value)})
|
||||||
|
document.getElementById("songsearch").addEventListener('keydown', function(e){searchSongsEnter(e)});
|
||||||
|
document.getElementById("iptextbox").addEventListener('keydown', function(e){ipSetEnter(e)});
|
||||||
|
document.getElementById("alerttimetextbox").addEventListener('keydown', function(e){alertTimeEnter(e)});
|
||||||
|
document.getElementById("partymode-button").addEventListener('click',function(){controlButton("pm")})
|
||||||
|
//sets the fact that clicking a song needs to return its id to the function to find it
|
||||||
|
document.getElementById("songlist").addEventListener('click', function(e){checkWhatSongWasClicked(e)});
|
||||||
|
//makes the controls look mostly normal on all screens, best solution i could find, idk man
|
||||||
|
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
|
||||||
|
//using this allows the creator of the link for, a qr code for example, to set the ip before distributing the code, and it would all work smoothly
|
||||||
|
//example (http://192.168.1.100:8000/?ip=192.168.1.100:19054 sets the ip to the same host at the default port)
|
||||||
|
//the port must be set manually using this method, but only has to be done once for the url that ends up being shared
|
||||||
|
let params = new URLSearchParams(location.search);
|
||||||
|
//tries the url first, then the cookie, then the default
|
||||||
|
ip = params.get("ip")
|
||||||
|
if (ip == null || ip=="") {
|
||||||
|
ip=getCookie("ip")
|
||||||
|
}
|
||||||
|
console.log(ip)
|
||||||
|
if (ip==null || ip==""){
|
||||||
|
ip = ""
|
||||||
|
}
|
||||||
|
document.cookie = "ip="+ip+"; path=/;"
|
||||||
|
|
||||||
|
|
||||||
|
alertTime = getCookie("alertTime")
|
||||||
|
document.getElementById("alerttimetextbox").value = alertTime
|
||||||
|
if (alertTime == "") {
|
||||||
|
alertTime = 2;
|
||||||
|
document.cookie = "alertTime="+alertTime+"; path=/;"
|
||||||
|
}
|
||||||
170
Client/styles.css
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
/* testing */
|
||||||
|
|
||||||
|
/* Things that are always visible */
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #EEEEEE;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
font-family: 'arial';
|
||||||
|
}
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
.clear{
|
||||||
|
clear: both;
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
max-width: 550px;
|
||||||
|
min-width: 300px;
|
||||||
|
position: fixed;
|
||||||
|
width:100%;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color:inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10%;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: #EEEEEEd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-button {
|
||||||
|
width: 15%;
|
||||||
|
max-width: 90px;
|
||||||
|
position:fixed;
|
||||||
|
top:0;
|
||||||
|
right:0;
|
||||||
|
margin: 3px;
|
||||||
|
background:inherit;
|
||||||
|
/* This is a circle background for the circle settings button
|
||||||
|
So it can display over other text and such */
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button{
|
||||||
|
width:20%;
|
||||||
|
max-width: 110px;
|
||||||
|
margin: auto 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
width: 300px;
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Songlist stuff */
|
||||||
|
.songlist {
|
||||||
|
width: 60%;
|
||||||
|
min-width: 300px;
|
||||||
|
margin:auto auto 150px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songlist > .item{
|
||||||
|
border: 1px solid #333333;
|
||||||
|
width:30%;
|
||||||
|
max-width: 200px;
|
||||||
|
margin: 5px auto;
|
||||||
|
min-width: 100px;
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songlist > .item > img{
|
||||||
|
max-width:200px;
|
||||||
|
width:100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.songlist > .item > h3, .songlist > .item > h4{
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbox-holder {
|
||||||
|
width: 20%;
|
||||||
|
margin: 20px auto 0;
|
||||||
|
min-width: 250px;
|
||||||
|
|
||||||
|
}
|
||||||
|
.searchbox {
|
||||||
|
width: 65%;
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
.go-search {
|
||||||
|
width: 20%;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
/* playlist mode stuff */
|
||||||
|
|
||||||
|
.playlist {
|
||||||
|
width: 60%;
|
||||||
|
min-width: 300px;
|
||||||
|
margin:auto auto 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist > .item{
|
||||||
|
border: 1px solid #333333;
|
||||||
|
display: flex;
|
||||||
|
max-width: 50em;
|
||||||
|
min-width: 200px;
|
||||||
|
margin: 5px auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist > .item > .text {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist > .item > img {
|
||||||
|
display: inline-flex;
|
||||||
|
max-width: 100px;
|
||||||
|
width:30%;
|
||||||
|
margin: 0;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist > .item > .text > * {
|
||||||
|
margin:5% 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* settings stuff */
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
width: 95%;
|
||||||
|
margin: auto auto 150px;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings > .item {
|
||||||
|
margin-left: 10%;
|
||||||
|
width:fit-content;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings > .item:not(:last-child) {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings > .lastSet1 {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#volumerange {
|
||||||
|
background-color: #4477AA;
|
||||||
|
color: #4477ff;
|
||||||
|
}
|
||||||
BIN
Screenshot_MAIN.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB |
77
Server/databaseGenerator.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import os
|
||||||
|
from mutagen.easyid3 import EasyID3
|
||||||
|
from mutagen.mp3 import MP3
|
||||||
|
import requests, ast, time, math, argparse, json
|
||||||
|
# place your lastfm key in the slot below
|
||||||
|
apikeylastfm="YourLastfmKeyHere"
|
||||||
|
|
||||||
|
loading = ["-","\\","|","/"]
|
||||||
|
songFiles = os.listdir(r'./sound')
|
||||||
|
parser=argparse.ArgumentParser(description="Options for the generation of the song database")
|
||||||
|
parser.add_argument('-m', '--mode', help='new/update: Remake database or update current', default= "update")
|
||||||
|
parser.add_argument('-a', '--art', help="True/False: Add art to the database using LastFm (takes minimum 0.25s per song)", default="True")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.mode == "update":
|
||||||
|
try:
|
||||||
|
with open('songDatabase.json', 'r') as handle:
|
||||||
|
songDatabaseList = json.load(handle)
|
||||||
|
except:
|
||||||
|
songDatabaseList=[]
|
||||||
|
|
||||||
|
for i in songDatabaseList:
|
||||||
|
try:
|
||||||
|
songFiles.index(i["file"]) != -1
|
||||||
|
except:
|
||||||
|
print("deleted: " + i["file"] + " from database")
|
||||||
|
songDatabaseList.pop(songDatabaseList.index(i))
|
||||||
|
|
||||||
|
for i in songDatabaseList:
|
||||||
|
songFiles.pop(songFiles.index(i["file"]))
|
||||||
|
print("new songs: " + str(songFiles))
|
||||||
|
elif args.mode=="new":
|
||||||
|
songDatabaseList = []
|
||||||
|
|
||||||
|
if args.art.lower() == "true":
|
||||||
|
x = len(songFiles)*0.25
|
||||||
|
if x > 60:
|
||||||
|
print("ETA "+ str(x/60) + " minutes")
|
||||||
|
else:
|
||||||
|
print("ETA "+ str(x) + " seconds")
|
||||||
|
|
||||||
|
for i in songFiles:
|
||||||
|
try:
|
||||||
|
song = EasyID3("sound/"+i)
|
||||||
|
title = song['title'][0]
|
||||||
|
artist = song['artist'][0]
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
song = i.split("_")
|
||||||
|
title = song[0]
|
||||||
|
artist = song[1].split(".")[0]
|
||||||
|
except:
|
||||||
|
title = i
|
||||||
|
artist = None
|
||||||
|
if args.art.lower() == "true":
|
||||||
|
try:
|
||||||
|
image = ast.literal_eval(requests.post(url="http://ws.audioscrobbler.com/2.0/?method=track.getInfo&api_key="+apikeylastfm+"&artist="+artist+"&track="+title+"&format=json").text)["track"]["album"]["image"][1]["#text"]
|
||||||
|
if image == "":
|
||||||
|
image = ast.literal_eval(requests.post(url="http://ws.audioscrobbler.com/2.0/?method=track.getInfo&api_key="+apikeylastfm+"&artist="+artist+"&track="+title+"&format=json").text)["track"]["album"]["image"][2]["#text"]
|
||||||
|
if image == "":
|
||||||
|
image = None
|
||||||
|
time.sleep(0.25)
|
||||||
|
except:
|
||||||
|
image=None
|
||||||
|
else:
|
||||||
|
image=None
|
||||||
|
try:
|
||||||
|
length = math.ceil(MP3("sound/"+i).info.length)
|
||||||
|
except:
|
||||||
|
length = 0
|
||||||
|
if len(songFiles) != 1:
|
||||||
|
index = (songFiles.index(i))%4
|
||||||
|
print("\r" + str(loading[index] + str(math.floor((songFiles.index(i)/(len(songFiles)-1))*100))+ "%"), end='', flush=True)
|
||||||
|
# each "song" is stored as a dictionary containing the below stuff, and each dictionary is put into a list
|
||||||
|
songDatabaseList.append({"file":i,"title":title,"artist":artist,"art":image,"length":length})
|
||||||
|
|
||||||
|
with open('songDatabase.json', 'w') as handle:
|
||||||
|
json.dump(songDatabaseList, handle)
|
||||||
128
Server/webbyBits.py
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
from flask import Flask
|
||||||
|
from flask import request
|
||||||
|
from flask_cors import CORS
|
||||||
|
import json,vlc,csv,threading,time,random
|
||||||
|
random.seed()
|
||||||
|
global partyMode
|
||||||
|
global skipNow
|
||||||
|
global songNext
|
||||||
|
partyMode = False
|
||||||
|
songNext = None
|
||||||
|
skipNow = False
|
||||||
|
playlist = []
|
||||||
|
playlistLock = threading.Lock()
|
||||||
|
fakeplayer = vlc.Instance()
|
||||||
|
player = fakeplayer.media_player_new()
|
||||||
|
# for client side volume to work as well as possible, set system volume to 100 and control in app
|
||||||
|
player.audio_set_volume(100)
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
with open('./songDatabase.json', 'r') as handle:
|
||||||
|
songDatabaseList = json.load(handle)
|
||||||
|
|
||||||
|
def queueSong(song):
|
||||||
|
with playlistLock:
|
||||||
|
playlist.append(song)
|
||||||
|
# this is a loop that plays the songs and checks for playlist changes, skips, ect.
|
||||||
|
def playQueuedSongs():
|
||||||
|
global skipNow
|
||||||
|
global songNext
|
||||||
|
global partyMode
|
||||||
|
while True:
|
||||||
|
with playlistLock:
|
||||||
|
z = str(player.get_state())
|
||||||
|
|
||||||
|
if playlist and (z == "State.Ended" or z== "State.Stopped" or z == "State.NothingSpecial" or skipNow == True):
|
||||||
|
player.stop()
|
||||||
|
skipNow = False
|
||||||
|
songNext = playlist.pop(0)
|
||||||
|
media = fakeplayer.media_new("sound/"+songNext)
|
||||||
|
player.set_media(media)
|
||||||
|
player.play()
|
||||||
|
elif (len(playlist) == 0) and skipNow==True:
|
||||||
|
skipNow=False
|
||||||
|
songNext = None
|
||||||
|
player.stop()
|
||||||
|
elif (len(playlist) == 0) and (z == "State.Ended" or z == "State.NothingSpecial" or z=="State.Stopped"):
|
||||||
|
songNext = None
|
||||||
|
elif (len(playlist)<1) and (partyMode == True):
|
||||||
|
playlist.append(random.choice(songDatabaseList)["file"])
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
queueThread = threading.Thread(target=playQueuedSongs)
|
||||||
|
queueThread.daemon = True
|
||||||
|
queueThread.start()
|
||||||
|
|
||||||
|
@app.route("/controls", methods=['POST'])
|
||||||
|
def playerControls():
|
||||||
|
global skipNow
|
||||||
|
global media
|
||||||
|
global partyMode
|
||||||
|
recieveData=request.get_json(force=True)
|
||||||
|
if recieveData["control"] != None:
|
||||||
|
if recieveData["control"] == "play-pause":
|
||||||
|
player.pause()
|
||||||
|
return "200"
|
||||||
|
elif recieveData["control"] == "skip":
|
||||||
|
skipNow = True
|
||||||
|
# print(str(player.get_state()))
|
||||||
|
return "200"
|
||||||
|
else:
|
||||||
|
return "400"
|
||||||
|
|
||||||
|
@app.route("/settings", methods=['POST'])
|
||||||
|
def settingsControl():
|
||||||
|
global partyMode
|
||||||
|
recieveData = request.get_json(force=True)
|
||||||
|
if recieveData["setting"] == "volume":
|
||||||
|
player.audio_set_volume(int(recieveData["level"]))
|
||||||
|
return "200"
|
||||||
|
elif recieveData["setting"] == "getsettings":
|
||||||
|
x = {"partymode":partyMode,"volume":player.audio_get_volume()}
|
||||||
|
return x
|
||||||
|
elif recieveData["setting"] == "partymode-toggle":
|
||||||
|
partyMode = not(partyMode)
|
||||||
|
return "200"
|
||||||
|
else:
|
||||||
|
return "400"
|
||||||
|
@app.route("/search", methods=['POST'])
|
||||||
|
def searchSongDB():
|
||||||
|
recieveData=request.get_json(force=True)
|
||||||
|
# the way i put the data in a list was really dumb looking back, i could and should have used a list of dictioaries like i was before
|
||||||
|
# i might try to change it but this layout is embedded deep in the client
|
||||||
|
tempData = {}
|
||||||
|
for i in songDatabaseList:
|
||||||
|
if ((i["title"].lower().find(recieveData['search'].lower())) > -1) or (recieveData['search'] == ""):
|
||||||
|
tempData[i["title"]] = [i["artist"],i["art"],i["file"]]
|
||||||
|
try:
|
||||||
|
if (i["artist"].lower().find(recieveData['search'].lower()) > -1):
|
||||||
|
tempData[i["title"]] = [i["artist"],i["art"],i["file"]]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return tempData
|
||||||
|
|
||||||
|
@app.route("/songadd", methods=["POST"])
|
||||||
|
def songadd():
|
||||||
|
recieveData=request.get_json(force=True)
|
||||||
|
queueSong(recieveData['song'])
|
||||||
|
return "200"
|
||||||
|
@app.route("/playlist", methods=["POST"])
|
||||||
|
def getPlaylist():
|
||||||
|
global songNext
|
||||||
|
tempPlaylist = []
|
||||||
|
for k in songDatabaseList:
|
||||||
|
if k["file"] == songNext:
|
||||||
|
temp = k.copy()
|
||||||
|
temp["playing"] = True
|
||||||
|
temp["time"] = player.get_time()/1000
|
||||||
|
tempPlaylist.append(temp)
|
||||||
|
for i in playlist:
|
||||||
|
for j in songDatabaseList:
|
||||||
|
if j["file"] == i:
|
||||||
|
tempPlaylist.append(j)
|
||||||
|
return tempPlaylist
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host='0.0.0.0', port='19054')
|
||||||
|
|
||||||
16
readme.md
|
|
@ -3,7 +3,7 @@
|
||||||
## Purpose
|
## Purpose
|
||||||
The **Party Jukebox** is a program that allows many people to add music, skip songs, play, and pause from any web device to the same device and playlist. \
|
The **Party Jukebox** is a program that allows many people to add music, skip songs, play, and pause from any web device to the same device and playlist. \
|
||||||
This was created for a personal use case for parties, and is a simple, (mostly) functional solution to have a collective playlist for local mp3 files. \
|
This was created for a personal use case for parties, and is a simple, (mostly) functional solution to have a collective playlist for local mp3 files. \
|
||||||
Main strenghts compared to doing something similar using Spotify are that you can limit the songs that can be played to your selection. Songs can be chosen, but only from a list
|
The main advantage compared to doing something similar using Spotify is that you can limit the songs that can be played to your selection. Songs can be chosen, but only from a list.
|
||||||
## Basic Setup
|
## Basic Setup
|
||||||
### Client Setup:
|
### Client Setup:
|
||||||
The client is a web application that can be hosted on any server, it need not be the same device running the music player.
|
The client is a web application that can be hosted on any server, it need not be the same device running the music player.
|
||||||
|
|
@ -11,7 +11,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:
|
||||||
|
|
||||||
```
|
```
|
||||||
sound/
|
sound/
|
||||||
|
|
@ -27,6 +27,8 @@ webbyBits.py
|
||||||
4. Run `webbyBits.py`
|
4. Run `webbyBits.py`
|
||||||
|
|
||||||
You can now connect with the client and use the app as normal. \
|
You can now connect with the client and use the app as normal. \
|
||||||
|
*Make sure you have turned down/off any other apps that might make noise or notification sounds* \
|
||||||
|
\
|
||||||
Read on for specific information on each piece of the app.
|
Read on for specific information on each piece of the app.
|
||||||
## Details
|
## Details
|
||||||
These are specific details on each section of the app, and how to use them
|
These are specific details on each section of the app, and how to use them
|
||||||
|
|
@ -38,6 +40,7 @@ These are specific details on each section of the app, and how to use them
|
||||||
- Art is retrieved from LastFM
|
- Art is retrieved from LastFM
|
||||||
- Running with `--mode (update/new)` either updates the current database and only adds new songs, or recreates the entire database (update is default)
|
- Running with `--mode (update/new)` either updates the current database and only adds new songs, or recreates the entire database (update is default)
|
||||||
- Running with `--art (True/False)` retrieves art from LastFM or doesn't (True is default)
|
- Running with `--art (True/False)` retrieves art from LastFM or doesn't (True is default)
|
||||||
|
- *Can only generate one song / 0.25 seconds, to avoid pinging the LastFM server too much*
|
||||||
- `songDatabase.json` stores all the information about each song in this format:
|
- `songDatabase.json` stores all the information about each song in this format:
|
||||||
```
|
```
|
||||||
[
|
[
|
||||||
|
|
@ -53,11 +56,13 @@ These are specific details on each section of the app, and how to use them
|
||||||
- `webbyBits.py` imports the database, runs all music playing, and accepts all commands from clients
|
- `webbyBits.py` imports the database, runs all music playing, and accepts all commands from clients
|
||||||
- Searches return matching songs
|
- Searches return matching songs
|
||||||
- Accepts Play-Pause and Skip commands
|
- Accepts Play-Pause and Skip commands
|
||||||
|
- Uses port 19054 by default
|
||||||
|
|
||||||
### Client:
|
### Client:
|
||||||
 \
|
 \
|
||||||
From left to right:
|
From left to right:
|
||||||
- The playlist button shows the current queue of songs
|
- The playlist button shows the current queue of songs
|
||||||
|
- The currently playing song is identified, and has the duration listed
|
||||||
- The play-pause button toggles playing
|
- The play-pause button toggles playing
|
||||||
- The skip button goes to the next track
|
- The skip button goes to the next track
|
||||||
- The search button opens the search screen (pictured)
|
- The search button opens the search screen (pictured)
|
||||||
|
|
@ -65,6 +70,5 @@ From left to right:
|
||||||
- Server IP allows you to change the ip that the site connects to
|
- Server IP allows you to change the ip that the site connects to
|
||||||
- Alert time changes how long error/confirmation messages are shown for (Default 2s)
|
- Alert time changes how long error/confirmation messages are shown for (Default 2s)
|
||||||
- Party Mode adds new songs to the queue when the queue has only 1 song in it
|
- Party Mode adds new songs to the queue when the queue has only 1 song in it
|
||||||
- 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*
|
||||||
|
|
||||||