Combining all my work #1

Merged
kristy-fournier merged 10 commits from dev into main 2024-06-12 13:01:31 -04:00
17 changed files with 830 additions and 7 deletions

3
.gitignore vendored
View file

@ -1,2 +1,3 @@
server/sound/
server/songDatabase.json
server/songDatabase.json
start.bat

BIN
Client/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
Client/images/playlist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
Client/images/search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
Client/images/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
Client/images/skip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

97
Client/index.html Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View 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
View 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')

View file

@ -3,7 +3,7 @@
## 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. \
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
### 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.
@ -11,7 +11,7 @@ The client is a web application that can be hosted on any server, it need not be
### 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 \
\
The server side consists of 3 files
The server side consists of 3 files:
```
sound/
@ -27,6 +27,8 @@ webbyBits.py
4. Run `webbyBits.py`
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.
## Details
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
- 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)
- *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:
```
[
@ -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
- Searches return matching songs
- Accepts Play-Pause and Skip commands
- Uses port 19054 by default
### Client:
![image](./Screenshot_RAW.png) \
![image](./Screenshot_MAIN.png) \
From left to right:
- 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 skip button goes to the next track
- 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
- 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
- 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*