Admin mode #5
5 changed files with 167 additions and 23 deletions
|
|
@ -81,13 +81,30 @@ changes visibility with JS-->
|
||||||
<p class="italic">Volume of the music</p>
|
<p class="italic">Volume of the music</p>
|
||||||
<input type="range" min="0" max="100" step="1" title="volumerange" id="volumerange">
|
<input type="range" min="0" max="100" step="1" title="volumerange" id="volumerange">
|
||||||
</div>
|
</div>
|
||||||
<div class="item no-line">
|
<div class="item">
|
||||||
<h2>Share the remote:</h2>
|
<h2>Share the remote:</h2>
|
||||||
<!-- <p class="italic">Hit settings icon to refresh the code</p>
|
<!-- <p class="italic">Hit settings icon to refresh the code</p>
|
||||||
You actually no longer need to do that, it does it anytime the ip changes -->
|
You actually no longer need to do that, it does it anytime the ip changes -->
|
||||||
<div id="qrcode" alt="QR code to the remote URL"></div>
|
<div id="qrcode" alt="QR code to the remote URL"></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="versionNumber">Version 1.0.2</p>
|
<h1>Admin Settings</h1>
|
||||||
|
<p class="italic">Note: Admin password must have been set from the server</p>
|
||||||
|
<div class="item">
|
||||||
|
<h2>Admin Password:</h2>
|
||||||
|
<p class="italic">Enter to use admin restricted functions</p>
|
||||||
|
<input placeholder="Wordpass12" type="password" title="Admin password box" id="adminpasswordbox">
|
||||||
|
</div>
|
||||||
|
<div class=item>
|
||||||
|
<h2>Fine action control:</h2>
|
||||||
|
<p class="italic">A check means that action is avalible to everyone</p>
|
||||||
|
<div id="admincheckholder">
|
||||||
|
<input type="checkbox" title="addsongcheck" id="addsongsettingcheckbox"><label for="addsongsettingcheckbox">Add songs to queue</label><br>
|
||||||
|
<input type="checkbox" title="skipsongcheck" id="skipsongsettingcheckbox"><label for="skipsongsettingcheckbox">Skip songs</label><br>
|
||||||
|
<input type="checkbox" title="playpausecheck" id="playpausesettingcheckbox"><label for="playpausesettingcheckbox">Play/pause</label><br>
|
||||||
|
<input type="checkbox" title="partymodecheck" id="partymodesettingcheckbox"><label for="partymodesettingcheckbox">Toggle Party Mode</label><br>
|
||||||
|
<input type="checkbox" title="volumechangecheck" id="volumechangesettingcheckbox"><label for="volumechangesettingcheckbox">Change volume</label><br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p class="versionNumber">PartyJukebox is under an <a href="https://github.com/kristy-fournier/PartyJukebox/blob/main/LICENSE.md">AGPLV3</a> liscense. You can access the source code <a href=https://github.com/kristy-fournier/PartyJukebox>here</a>.</p>
|
<p class="versionNumber">PartyJukebox is under an <a href="https://github.com/kristy-fournier/PartyJukebox/blob/main/LICENSE.md">AGPLV3</a> liscense. You can access the source code <a href=https://github.com/kristy-fournier/PartyJukebox>here</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
let ip
|
let ip;
|
||||||
let alertTime = 2
|
let alertTime = 2;
|
||||||
|
let adminPass = "";
|
||||||
|
const ERR_NO_ADMIN = "401"; // gonna use this later to refactor
|
||||||
|
|
||||||
async function alertText(text="Song Added!") {
|
async function alertText(text="Song Added!") {
|
||||||
alertbox = document.getElementById("alert");
|
alertbox = document.getElementById("alert");
|
||||||
alertbox.innerHTML = text;
|
alertbox.innerHTML = text;
|
||||||
|
|
@ -10,8 +13,11 @@ async function alertText(text="Song Added!") {
|
||||||
}
|
}
|
||||||
// a lot of this is kinda waffly because i was trying to get
|
// a lot of this is kinda waffly because i was trying to get
|
||||||
// it to return the right stuff and javascript is asyrcronouse (boo)
|
// it to return the right stuff and javascript is asyrcronouse (boo)
|
||||||
async function getFromServer(bodyInfo, source="") {
|
async function getFromServer(bodyInfo, source="",password=adminPass) {
|
||||||
try{
|
try{
|
||||||
|
if (bodyInfo != null) {
|
||||||
|
bodyInfo["password"] = password;
|
||||||
|
}
|
||||||
const response = await fetch("http://"+ip+"/"+source, {
|
const response = await fetch("http://"+ip+"/"+source, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(bodyInfo),
|
body: JSON.stringify(bodyInfo),
|
||||||
|
|
@ -20,10 +26,15 @@ async function getFromServer(bodyInfo, source="") {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
if (data == "401") {
|
||||||
|
alertText("Error: Admin restricted action")
|
||||||
|
}
|
||||||
return await data;
|
return await data;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
if (e == "TypeError: Failed to fetch"){
|
if (e == "TypeError: Failed to fetch"){
|
||||||
alertText("error: Can't Connect to Server (is the ip set?)")
|
alertText("Error: Can't Connect to Server (is the ip set?)")
|
||||||
|
} else if(e == "") {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
alertText("error: " + e)
|
alertText("error: " + e)
|
||||||
}
|
}
|
||||||
|
|
@ -192,6 +203,14 @@ async function checkSettings(skipServer=false) {
|
||||||
qrCodeGenerate()
|
qrCodeGenerate()
|
||||||
document.getElementById("alerttimetextbox").value = alertTime
|
document.getElementById("alerttimetextbox").value = alertTime
|
||||||
partyButtonState = document.getElementById("partymode-button").innerHTML;
|
partyButtonState = document.getElementById("partymode-button").innerHTML;
|
||||||
|
let nodeList = document.getElementById("admincheckholder").children
|
||||||
|
// temporary
|
||||||
|
for (let i=0; i<nodeList.length;i++) {
|
||||||
|
if (nodeList[i].type == 'checkbox') {
|
||||||
|
nodeList[i].checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//ping the server here
|
||||||
x = await getFromServer({setting: "getsettings"}, "settings");
|
x = await getFromServer({setting: "getsettings"}, "settings");
|
||||||
if (!(skipServer) || partyButtonState=="N/A") {
|
if (!(skipServer) || partyButtonState=="N/A") {
|
||||||
if (x["partymode"] == false) {
|
if (x["partymode"] == false) {
|
||||||
|
|
@ -205,6 +224,14 @@ async function checkSettings(skipServer=false) {
|
||||||
document.getElementById("partymode-button").innerHTML = "Off";
|
document.getElementById("partymode-button").innerHTML = "Off";
|
||||||
}
|
}
|
||||||
document.getElementById("volumerange").value = parseInt(x["volume"])
|
document.getElementById("volumerange").value = parseInt(x["volume"])
|
||||||
|
|
||||||
|
// do the admin checkboxes here
|
||||||
|
let currentAdminPerms = x["admin"];
|
||||||
|
document.getElementById("addsongsettingcheckbox").checked = currentAdminPerms["AS"];
|
||||||
|
document.getElementById("skipsongsettingcheckbox").checked = currentAdminPerms["SK"];
|
||||||
|
document.getElementById("playpausesettingcheckbox").checked = currentAdminPerms["PP"];
|
||||||
|
document.getElementById("partymodesettingcheckbox").checked = currentAdminPerms["PM"];
|
||||||
|
document.getElementById("volumechangesettingcheckbox").checked = currentAdminPerms["VOL"];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateVisualPlaylist(conditions="") {
|
async function generateVisualPlaylist(conditions="") {
|
||||||
|
|
@ -275,8 +302,12 @@ async function generateVisualPlaylist(conditions="") {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitSong(songid) {
|
async function submitSong(songid) {
|
||||||
getFromServer({song: songid}, "songadd")
|
let returncode = await getFromServer({song: songid}, "songadd");
|
||||||
alertText("Added to Queue")
|
if(returncode == ERR_NO_ADMIN) {
|
||||||
|
// right now the error is alerted in getFromServer, maybe will change that
|
||||||
|
} else {
|
||||||
|
alertText("Added to Queue");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function checkWhatSongWasClicked(e) {
|
function checkWhatSongWasClicked(e) {
|
||||||
itemId = e.srcElement.id;
|
itemId = e.srcElement.id;
|
||||||
|
|
@ -301,6 +332,28 @@ function toggleDark(e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
function adminPassEnter(e) {
|
||||||
|
if (e.key == "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
adminPass=document.getElementById("adminpasswordbox").value
|
||||||
|
alertText("Admin Password Updated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function submitPerms(e) {
|
||||||
|
let tempData = {}
|
||||||
|
tempData["PP"] = document.getElementById("playpausesettingcheckbox").checked;
|
||||||
|
tempData["SK"] = document.getElementById("skipsongsettingcheckbox").checked;
|
||||||
|
tempData["AS"] = document.getElementById("addsongsettingcheckbox").checked;
|
||||||
|
tempData["PM"] = document.getElementById("partymodesettingcheckbox").checked;
|
||||||
|
tempData["VOL"] = document.getElementById("partymodesettingcheckbox").checked;
|
||||||
|
let returncode = await getFromServer({"setting":"perms","admin":tempData},"settings");
|
||||||
|
if (returncode == ERR_NO_ADMIN || returncode == null) {
|
||||||
|
// if you aren't allowed to check the box then toggle it again
|
||||||
|
// its not perfect if you spam click, but it gets the point across to the user
|
||||||
|
let clickedBox = e.srcElement;
|
||||||
|
clickedBox.checked = !clickedBox.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let optionslist = []
|
let optionslist = []
|
||||||
|
|
||||||
|
|
@ -317,6 +370,8 @@ document.getElementById("settings-mode").style.display = "none";
|
||||||
document.getElementById("volumerange").onchange = async function() {
|
document.getElementById("volumerange").onchange = async function() {
|
||||||
let returnValue = await getFromServer({setting:"volume",level:this.value}, "settings")
|
let returnValue = await getFromServer({setting:"volume",level:this.value}, "settings")
|
||||||
if (returnValue["volumePassed"] !=0) {
|
if (returnValue["volumePassed"] !=0) {
|
||||||
|
// i forgot about this, i had to do this because it confused the crap out of me one time
|
||||||
|
// vlc doesn't let you change the volume of nothing, which makes sense if you think about it
|
||||||
alertText("Nothing is playing")
|
alertText("Nothing is playing")
|
||||||
document.getElementById("volumerange").value = -1
|
document.getElementById("volumerange").value = -1
|
||||||
}
|
}
|
||||||
|
|
@ -336,6 +391,8 @@ document.getElementById("go-search").addEventListener('click', function(){search
|
||||||
document.getElementById("songsearch").addEventListener('keydown', function(e){searchSongsEnter(e)});
|
document.getElementById("songsearch").addEventListener('keydown', function(e){searchSongsEnter(e)});
|
||||||
document.getElementById("iptextbox").addEventListener('keydown', function(e){ipSetEnter(e)});
|
document.getElementById("iptextbox").addEventListener('keydown', function(e){ipSetEnter(e)});
|
||||||
document.getElementById("alerttimetextbox").addEventListener('keydown', function(e){alertTimeEnter(e)});
|
document.getElementById("alerttimetextbox").addEventListener('keydown', function(e){alertTimeEnter(e)});
|
||||||
|
document.getElementById("adminpasswordbox").addEventListener('keydown',function(e){adminPassEnter(e)});
|
||||||
|
document.getElementById("admincheckholder").addEventListener('click',function(e){submitPerms(e)});
|
||||||
document.getElementById("partymode-button").addEventListener('click',function(){controlButton("pm")})
|
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
|
//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)});
|
document.getElementById("songlist").addEventListener('click', function(e){checkWhatSongWasClicked(e)});
|
||||||
|
|
@ -343,6 +400,7 @@ document.getElementById("songlist").addEventListener('click', function(e){checkW
|
||||||
let tempWidth = document.getElementById('controls').clientWidth;
|
let tempWidth = document.getElementById('controls').clientWidth;
|
||||||
document.getElementById("controls").style.marginLeft = "-"+String(parseInt(tempWidth/2))+"px";
|
document.getElementById("controls").style.marginLeft = "-"+String(parseInt(tempWidth/2))+"px";
|
||||||
// document.getElementById("darkmode-button").addEventListener('click',function(){toggleDark()})
|
// document.getElementById("darkmode-button").addEventListener('click',function(){toggleDark()})
|
||||||
|
|
||||||
//for my use case (my immediate family), they dont know how to set an ip
|
//for my use case (my immediate family), they dont know how to set an ip
|
||||||
//using this allows the creator of the link for, a qr code for example, to set the ip before distributing the code, and it would all work smoothly
|
//using this allows the creator of the link for, a qr code for example, to set the ip before distributing the code, and it would all work smoothly
|
||||||
//example (http://192.168.1.100:8000/?ip=192.168.1.100:19054 sets the ip to the same host at the default port)
|
//example (http://192.168.1.100:8000/?ip=192.168.1.100:19054 sets the ip to the same host at the default port)
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,14 @@ h4 {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings > .item > h2 {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings > .item > p {
|
||||||
|
margin-top: 0px
|
||||||
|
}
|
||||||
|
|
||||||
.versionNumber {
|
.versionNumber {
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,26 @@ parser=argparse.ArgumentParser(description="Options for the Webby Bits")
|
||||||
# this is no longer needed assuming my file works correctly with the generator
|
# this is no longer needed assuming my file works correctly with the generator
|
||||||
# parser.add_argument('-d','--directory',help="Directory of the song files (make sure this matches the directory used for the databaseGenerator)", default="./sound/")
|
# parser.add_argument('-d','--directory',help="Directory of the song files (make sure this matches the directory used for the databaseGenerator)", default="./sound/")
|
||||||
parser.add_argument('-p','--port',help="Port to host on, not the same as the web (client) port",default='19054')
|
parser.add_argument('-p','--port',help="Port to host on, not the same as the web (client) port",default='19054')
|
||||||
portTheUserPicked=parser.parse_args().port
|
parser.add_argument('-a','--admin',help="Add an admin password to be used in the client. DO NOT use a password you use elsewhere",default="")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
portTheUserPicked=args.port
|
||||||
|
# Just a note that the return code "401" as of now is used to mean "you don't have the password"
|
||||||
|
# This is not great design, and the whole "returning string codes" thing is something to add to the todo list
|
||||||
|
# I mean returning 200 when no return is necesary i think is fine but we'll see
|
||||||
|
ERR_NO_ADMIN = "401"
|
||||||
|
ADMIN_PASS = args.admin
|
||||||
|
if not(ADMIN_PASS):
|
||||||
|
ADMIN_PASS = None
|
||||||
|
# True = everyone, False = admin only. Change in client while in use.
|
||||||
|
controlPerms = {
|
||||||
|
"PP":True, #done
|
||||||
|
"SK":True, #done
|
||||||
|
"AS":True, #done
|
||||||
|
"PM":True, #done
|
||||||
|
"VOL":True #done
|
||||||
|
}
|
||||||
|
|
||||||
fileofDB = sql.connect("songDatabase.db")
|
fileofDB = sql.connect("songDatabase.db")
|
||||||
songDatabase = fileofDB.cursor()
|
songDatabase = fileofDB.cursor()
|
||||||
|
|
@ -23,7 +42,7 @@ elif "/" in soundLocation:
|
||||||
soundLocation += "/"
|
soundLocation += "/"
|
||||||
else:
|
else:
|
||||||
soundLocation += "\\"
|
soundLocation += "\\"
|
||||||
print(soundLocation)
|
#print(soundLocation)
|
||||||
#Create Virtual table for searching
|
#Create Virtual table for searching
|
||||||
songDatabase.execute("DROP TABLE virtualSongs;")
|
songDatabase.execute("DROP TABLE virtualSongs;")
|
||||||
songDatabase.execute("CREATE VIRTUAL TABLE virtualSongs USING fts5(filename, title, artist, art, length);")
|
songDatabase.execute("CREATE VIRTUAL TABLE virtualSongs USING fts5(filename, title, artist, art, length);")
|
||||||
|
|
@ -45,7 +64,7 @@ player = fakeplayer.media_player_new()
|
||||||
# for client side volume to work as well as possible, set system volume to 100 and control in app
|
# for client side volume to work as well as possible, set system volume to 100 and control in app
|
||||||
player.audio_set_volume(100)
|
player.audio_set_volume(100)
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
# because you are posting from another domain to this one, you need CORS
|
# because you are POSTing from another domain to this one, you need CORS
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
def queueSong(song):
|
def queueSong(song):
|
||||||
|
|
@ -98,30 +117,53 @@ def playerControls():
|
||||||
recieveData=request.get_json(force=True)
|
recieveData=request.get_json(force=True)
|
||||||
if recieveData["control"] != None:
|
if recieveData["control"] != None:
|
||||||
if recieveData["control"] == "play-pause":
|
if recieveData["control"] == "play-pause":
|
||||||
player.pause()
|
if ADMIN_PASS == recieveData['password'] or not(ADMIN_PASS) or controlPerms["PP"]:
|
||||||
return "200"
|
player.pause()
|
||||||
|
return "200"
|
||||||
|
else:
|
||||||
|
return ERR_NO_ADMIN
|
||||||
elif recieveData["control"] == "skip":
|
elif recieveData["control"] == "skip":
|
||||||
skipNow = True
|
if ADMIN_PASS == recieveData['password'] or not(ADMIN_PASS) or controlPerms["SK"]:
|
||||||
# print(str(player.get_state()))
|
skipNow = True
|
||||||
return "200"
|
return "200"
|
||||||
|
else:
|
||||||
|
return ERR_NO_ADMIN
|
||||||
else:
|
else:
|
||||||
return "400"
|
return "400"
|
||||||
|
else:
|
||||||
|
return "400"
|
||||||
|
|
||||||
@app.route("/settings", methods=['POST'])
|
@app.route("/settings", methods=['POST'])
|
||||||
def settingsControl():
|
def settingsControl():
|
||||||
|
global controlPerms
|
||||||
# set the volume and partymode
|
# set the volume and partymode
|
||||||
global partyMode
|
global partyMode
|
||||||
global player
|
global player
|
||||||
recieveData = request.get_json(force=True)
|
recieveData = request.get_json(force=True)
|
||||||
if recieveData["setting"] == "volume":
|
if recieveData["setting"] == "volume":
|
||||||
volumePassed = player.audio_set_volume(int(recieveData["level"]))
|
if ADMIN_PASS == recieveData['password'] or not(ADMIN_PASS) or controlPerms["VOL"]:
|
||||||
return {"volumePassed":volumePassed}
|
volumePassed = player.audio_set_volume(int(recieveData["level"]))
|
||||||
|
return {"volumePassed":volumePassed}
|
||||||
|
else:
|
||||||
|
return ERR_NO_ADMIN
|
||||||
elif recieveData["setting"] == "partymode-toggle":
|
elif recieveData["setting"] == "partymode-toggle":
|
||||||
partyMode = not(partyMode)
|
if ADMIN_PASS == recieveData['password'] or not(ADMIN_PASS) or controlPerms["PM"]:
|
||||||
return "200"
|
partyMode = not(partyMode)
|
||||||
|
return "200"
|
||||||
|
else:
|
||||||
|
return ERR_NO_ADMIN
|
||||||
|
elif recieveData["setting"] == "perms":
|
||||||
|
# print(ADMIN_PASS)
|
||||||
|
# print(recieveData["password"])
|
||||||
|
if ADMIN_PASS == recieveData["password"] and ADMIN_PASS:
|
||||||
|
#if an adminpass doesn't exist these perms can never be changed
|
||||||
|
controlPerms = recieveData["admin"]
|
||||||
|
return "200"
|
||||||
|
else:
|
||||||
|
return ERR_NO_ADMIN
|
||||||
elif recieveData["setting"] == "getsettings":
|
elif recieveData["setting"] == "getsettings":
|
||||||
# probably should have made this a different request type or something but it works
|
# probably should have made this a different request type or something but it works
|
||||||
x = {"partymode":partyMode,"volume":player.audio_get_volume()}
|
x = {"partymode":partyMode,"volume":player.audio_get_volume(),"admin":controlPerms}
|
||||||
return x
|
return x
|
||||||
else:
|
else:
|
||||||
return "400"
|
return "400"
|
||||||
|
|
@ -154,8 +196,14 @@ def searchSongDB():
|
||||||
@app.route("/songadd", methods=["POST"])
|
@app.route("/songadd", methods=["POST"])
|
||||||
def songadd():
|
def songadd():
|
||||||
recieveData=request.get_json(force=True)
|
recieveData=request.get_json(force=True)
|
||||||
queueSong(recieveData['song'])
|
if (ADMIN_PASS and ADMIN_PASS == recieveData['password']):
|
||||||
return "200"
|
# Pass exists and is correct, or it's not restricted
|
||||||
|
queueSong(recieveData['song'])
|
||||||
|
return "200"
|
||||||
|
else:
|
||||||
|
# Pass exists, or this action isn't restricted
|
||||||
|
return ERR_NO_ADMIN
|
||||||
|
|
||||||
@app.route("/playlist", methods=["POST"])
|
@app.route("/playlist", methods=["POST"])
|
||||||
def getPlaylist():
|
def getPlaylist():
|
||||||
global songNext
|
global songNext
|
||||||
|
|
|
||||||
13
readme.md
13
readme.md
|
|
@ -28,6 +28,9 @@ webbyBits.py
|
||||||
* *If getting images, this process may take a long time with a large amount of mp3 files*
|
* *If getting images, this process may take a long time with a large amount of mp3 files*
|
||||||
4. Run `webbyBits.py`
|
4. Run `webbyBits.py`
|
||||||
* *The port can be customized at runtime using* `-p portNumber` *as an atribute*
|
* *The port can be customized at runtime using* `-p portNumber` *as an atribute*
|
||||||
|
* *You can add an admin password at runtime with* `-a AdminPass` *as an atribute*
|
||||||
|
* ***NOTE: Do not reuse ANY password for this, it is 100% unsecure. The best option is just a random string you write down once***
|
||||||
|
* This is intended for protecting certain features for small closed events, not for public security
|
||||||
|
|
||||||
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* \
|
*Make sure you have turned down/off any other apps that might make noise or notification sounds* \
|
||||||
|
|
@ -57,6 +60,16 @@ These are specific details on each section of the app, and how to use them
|
||||||
- Uses port 19054 by default
|
- Uses port 19054 by default
|
||||||
- `--port (port)` changes the port for that run
|
- `--port (port)` changes the port for that run
|
||||||
- The default port can be changed in the file
|
- The default port can be changed in the file
|
||||||
|
- Running with `--admin (admin password)` sets an admin password for moderation on the client
|
||||||
|
- ***Note: Do not reuse a password, consider this like making whatever this string is public, no security is guaranteed***
|
||||||
|
- Anyone who knows the admin password can enter it on the client and change the abilities of any non-admin users (for example to limit skipping)
|
||||||
|
- The total set of features that can be restricted is
|
||||||
|
- Skip track
|
||||||
|
- Play-pause toggle
|
||||||
|
- Add track
|
||||||
|
- Partymode toggle
|
||||||
|
- Change volume
|
||||||
|
- When this argument is left out (or empty string) the admin features aren't used, and everyone can do everything
|
||||||
|
|
||||||
### Client:
|
### Client:
|
||||||
 \
|
 \
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue