PartyJukebox/Server/webbyBits.py

252 lines
No EOL
9.5 KiB
Python

from flask import Flask
from flask import request
from flask_cors import CORS
import sqlite3 as sql
import vlc,threading,time,random,argparse,dotenv,os,hashlib,string
# Argparse Stuff
parser=argparse.ArgumentParser(description="Options for the Webby Bits")
# parser.add_argument('-p','--port',help="Port to host on, not the same as the web (client) port",default='19054')
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()
dotenv.load_dotenv()
portTheUserPicked=os.getenv("SERVER_PORT")
ERR_NO_ADMIN = ({"error":"no-admin"},401)
ERR_200 = ({"error":"OK"},200)
if args.admin:
ADMIN_PASS = hashlib.sha256(bytes(args.admin,'utf-8')).hexdigest()
else:
tempPass = ''.join(random.choices(string.ascii_letters + string.digits +"?"+"!",k=20))
print("No adminPass was set, the auto generated one is: "+tempPass)
ADMIN_PASS = hashlib.sha256(bytes(tempPass,'utf-8')).hexdigest()
# True = everyone, False = admin only. Change in client while in use.
# play-pause,skip,addsong,partymode,volume in order
controlPerms = {
"PP":True,
"SK":True,
"AS":True,
"PM":True,
"VOL":True
}
fileofDB = sql.connect("songDatabase.db")
songDatabase = fileofDB.cursor()
#song directory
songDatabase.execute("SELECT * FROM meta WHERE id='songDirectory';")
soundLocation = songDatabase.fetchall()[0][1]
if soundLocation[-1] == "/" or soundLocation[-1] == "\\":
pass
elif "/" in soundLocation:
soundLocation += "/"
else:
soundLocation += "\\"
#Create Virtual table for searching
#I'm not sure why i don't do this in the databaseGenerator, but it also takes like 3 seconds so i'm not messing with it rn
#Initializing all the global stuff
random.seed()
global partyMode
global skipNow
global songNext
partyMode = False
songNext = None
skipNow = False
playlist = []
playlistLock = threading.Lock()
vlcInstance = vlc.Instance()
player = vlcInstance.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__)
# because you are POSTing from another domain to this one, you need CORS
CORS(app)
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:
playerState = str(player.get_state())
endStates = ["State.Ended","State.Stopped","State.NothingSpecial"]
if playlist and (playerState in endStates or skipNow == True):
# New song is in the queue and (the previous song is over or skip has been pressed)
player.stop()
skipNow = False
songNext = playlist.pop(0)
media = vlcInstance.media_new(soundLocation+songNext)
player.set_media(media)
player.play()
elif (skipNow==True or (playerState in endStates)):
# skip was pressed and there are no new songs
skipNow=False
songNext = None
player.stop()
elif len(playlist)<1 and (partyMode == True):
fileofDB = sql.connect("songDatabase.db")
songDatabase = fileofDB.cursor()
songDatabase.execute("SELECT * FROM songs ORDER BY RANDOM() LIMIT 1;")
result = songDatabase.fetchall()
# adds the random songs for party mode
# the above 2 means this only applies if (a song is playing or paused) and (the queue is empty)
playlist.append(result[0][0])
# check for new songs every second
# I just didn't want to eat too much processing looping
# this also has another useful affect that skips get "queued" to only 1 per second, that way somebody usually can't skip twice accidentally
time.sleep(1)
@app.route("/controls", methods=['POST'])
def playerControls():
# recieve control inputs (play/pause and skip) from the webUI
global skipNow
global partyMode
recieveData=request.get_json(force=True)
if recieveData["control"] != None:
if recieveData["control"] == "play-pause":
if ADMIN_PASS == recieveData['password'] or controlPerms["PP"]:
player.pause()
return ERR_200
else:
return ERR_NO_ADMIN
elif recieveData["control"] == "skip":
if ADMIN_PASS == recieveData['password'] or controlPerms["SK"]:
skipNow = True
return ERR_200
else:
return ERR_NO_ADMIN
# Maybe i should have put this next one in the "settings" section
elif recieveData["control"] == "clear":
if ADMIN_PASS == recieveData['password']: # this is only ever allowed with the adminpassword
with playlistLock:
playlist.clear()
return ERR_200
else:
return ERR_NO_ADMIN
else:
return {"error":"Not a valid control"},400
else:
return {"error":"No control sent"},400
@app.route("/settings", methods=['POST'])
def settingsControl():
global controlPerms
# set the volume and partymode
global partyMode
global player
recieveData = request.get_json(force=True)
if recieveData["setting"] == "volume":
if ADMIN_PASS == recieveData['password'] or controlPerms["VOL"]:
volumePassed = player.audio_set_volume(int(recieveData["level"]))
return {"volumePassed":volumePassed}
else:
return ERR_NO_ADMIN
elif recieveData["setting"] == "partymode-toggle":
if ADMIN_PASS == recieveData['password'] or controlPerms["PM"]:
partyMode = not(partyMode)
return ERR_200
else:
return ERR_NO_ADMIN
elif recieveData["setting"] == "perms":
if ADMIN_PASS == recieveData["password"]:
controlPerms = recieveData["admin"]
return ERR_200
else:
return ERR_NO_ADMIN
elif recieveData["setting"] == "getsettings":
# probably should have made this a different request type or something but it works
return {"partymode":partyMode,"volume":player.audio_get_volume(),"admin":controlPerms}
else:
return "400"
@app.route("/search", methods=['POST'])
def searchSongDB():
recieveData=request.get_json(force=True)
fileofDB = sql.connect("songDatabase.db")
songDatabase = fileofDB.cursor()
results = []
if (recieveData['search'] == ""):
songDatabase.execute("SELECT * FROM virtualSongs")
results = songDatabase.fetchall()
else:
songDatabase.execute("SELECT * FROM virtualSongs WHERE virtualSongs MATCH ?",[recieveData['search']])
results = songDatabase.fetchall()
tempdata = {}
# this is a temporary solution so i dont have to change the client
for i in results:
tempdata[i[0]] = {
"title": i[1],
"artist": i[2],
"art": i[3],
"length": i[4],
"lossless":i[5]
}
fileofDB.close()
return tempdata
@app.route("/songadd", methods=["POST"])
def songadd():
recieveData=request.get_json(force=True)
if (ADMIN_PASS and ADMIN_PASS == recieveData['password']) or controlPerms["AS"]:
# Password exists and is correct, or it's not restricted
# if (recieveData['song'] in playlist):
# return {"error":"song-in-queue"}
# else:
# Right now the above is disabled since i want to make it optional first
# probably with a checkbox like the other admin controls
if True:
queueSong(recieveData['song'])
return ERR_200
else:
# the password is incorrect (technically a password not existing falls into the above case because controlPerms is never changed)
return ERR_NO_ADMIN
@app.route("/playlist", methods=["POST"])
def getPlaylist():
global songNext
fileofDB = sql.connect("songDatabase.db")
songDatabase = fileofDB.cursor()
tempPlaylist = []
if songNext != None:
# Adds the currently playing song
songDatabase.execute("SELECT * FROM songs WHERE filename = ?",[songNext])
result = songDatabase.fetchall()[0]
# again, this is still using the old JSON format to avoid client changes
k = {
"title": result[1],
"artist": result[2],
"art": result[3],
"length": result[4]
}
temp = k.copy()
temp["playing"] = True
temp["time"] = player.get_time()/1000
tempPlaylist.append({songNext:temp})
for i in playlist:
songDatabase.execute("SELECT * FROM songs WHERE filename = ?",[i])
result = songDatabase.fetchall()[0]
k = {
"title": result[1],
"artist": result[2],
"art": result[3],
"length": result[4]
}
tempPlaylist.append({i:k})
fileofDB.close()
return tempPlaylist
if __name__ == "__main__":
# There's not really a whole lot of point to a main function for something like this, you'd never use any of these methods
# elsewhere, but its just good practice i guess
# start the media player thread
queueThread = threading.Thread(target=playQueuedSongs)
queueThread.daemon = True
queueThread.start()
app.run(host='0.0.0.0', port=portTheUserPicked)