Added all app files
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 |
90
Client/index.html
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<!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="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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
329
Client/scripts.js
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
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));
|
||||||
|
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;
|
||||||
|
}
|
||||||
78
Server/databaseGenerator.py
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import os
|
||||||
|
from mutagen.easyid3 import EasyID3
|
||||||
|
from mutagen.mp3 import MP3
|
||||||
|
import requests, ast, time, math, argparse, json
|
||||||
|
|
||||||
|
loading = ["-","\\","|","/"]
|
||||||
|
|
||||||
|
apikeylastfm="YourLastfmKeyHere"
|
||||||
|
|
||||||
|
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='25565')
|
||||||
|
|
||||||