init
This commit is contained in:
commit
b51b2db8db
|
@ -0,0 +1,276 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
import sys, os
|
||||||
|
import http.client
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from flask import Flask, render_template, request, make_response, jsonify
|
||||||
|
from waitress import serve
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
DEBUG = 0
|
||||||
|
video_folder = "/home/pi"
|
||||||
|
media_ext = [ "mp4", "avi", "mkv" ]
|
||||||
|
# ~ video_folder = "/media/"
|
||||||
|
## base64 encoded ":secret""
|
||||||
|
# import base64
|
||||||
|
# passwd = "foo"
|
||||||
|
# passswd64 = str(base64.b64encode(passwd.encode('utf-8')), 'utf-8')
|
||||||
|
auth = "OnNlY3JldA=="
|
||||||
|
cmd_auth = "OnNlY3JldA=="
|
||||||
|
hosts = [ "10.42.0.135", "10.42.0.156" ]
|
||||||
|
# VLC http LUA port
|
||||||
|
port = 8080
|
||||||
|
# Clients cmd port
|
||||||
|
cmd_port = 5000
|
||||||
|
|
||||||
|
hosts_available, hosts_unavailable = [],[]
|
||||||
|
|
||||||
|
# Map vlc cmds
|
||||||
|
# See https://github.com/videolan/vlc/blob/1336447566c0190c42a1926464fa1ad2e59adc4f/share/lua/http/requests/README.txt
|
||||||
|
cmd = {
|
||||||
|
"play" : "pl_play",
|
||||||
|
"resume" : "pl_forceresume",
|
||||||
|
"pause" : "pl_forcepause",
|
||||||
|
"tpause" : "pl_pause",
|
||||||
|
"previous" : "pl_previous",
|
||||||
|
"next" : "pl_next",
|
||||||
|
"stop" : "pl_stop",
|
||||||
|
"enqueue" : "in_enqueue",
|
||||||
|
"add" : "in_play",
|
||||||
|
"clear" : "pl_empty",
|
||||||
|
"delete" : "pl_delete",
|
||||||
|
"loop" : "pl_loop",
|
||||||
|
"repeat" : "pl_repeat",
|
||||||
|
"random" : "pl_random",
|
||||||
|
"move" : "pl_move",
|
||||||
|
"sort" : "pl_sort",
|
||||||
|
"seek" : "seek",
|
||||||
|
"sync" : "sync",
|
||||||
|
"status" : "status.xml",
|
||||||
|
"list" : "playlist.xml",
|
||||||
|
#"volume" : "volume",
|
||||||
|
#"ratio" : "aspectratio",
|
||||||
|
#"dir" : "?dir=<uri>",
|
||||||
|
#"command" : "?command=<cmd>",
|
||||||
|
#"key" : "key=",
|
||||||
|
#"browse" : "browse.xml?uri=file://~"
|
||||||
|
# System commands :
|
||||||
|
"rssi" : "rssi",
|
||||||
|
"blink" : "blink",
|
||||||
|
"poweroff" : "poweroff",
|
||||||
|
"reboot" : "reboot",
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# Network/link utilities
|
||||||
|
# https://www.metageek.com/training/resources/understanding-rssi/
|
||||||
|
|
||||||
|
def isup(host_l, port):
|
||||||
|
global DEBUG
|
||||||
|
import socket
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
try:
|
||||||
|
s.settimeout(3.0)
|
||||||
|
s.connect((host_l, port))
|
||||||
|
if DEBUG:
|
||||||
|
print( "Port " + str(port) + " reachable")
|
||||||
|
return 1
|
||||||
|
except (socket.error, socket.timeout) as e:
|
||||||
|
if DEBUG:
|
||||||
|
print("Error on connect to " + host_l + ": %s" % e)
|
||||||
|
return 0
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
def checkHosts(host_l):
|
||||||
|
hostdown, hostup = [], []
|
||||||
|
hosts_number = str(len(host_l))
|
||||||
|
for lhost in host_l:
|
||||||
|
if not isup(lhost, 8080):
|
||||||
|
hostdown.append(lhost)
|
||||||
|
else:
|
||||||
|
hostup.append(lhost)
|
||||||
|
if DEBUG:
|
||||||
|
print( str(len(hostup)) + " of " + hosts_number + " hosts found.")
|
||||||
|
return hostup, hostdown
|
||||||
|
|
||||||
|
# File utilities
|
||||||
|
|
||||||
|
def listMediaFiles(folder):
|
||||||
|
files = os.listdir(folder);
|
||||||
|
medias = []
|
||||||
|
for fd in files:
|
||||||
|
if len(fd.split('.')) > 1:
|
||||||
|
if fd.split('.')[1] in media_ext:
|
||||||
|
medias.append(fd)
|
||||||
|
return medias
|
||||||
|
|
||||||
|
# /requests/status.xml?command=in_enqueue&input=file:///home/pi/tst1.mp4
|
||||||
|
def writeM3U(m3u_content : str, host : str):
|
||||||
|
filename = host.replace(".", "_") + ".m3u"
|
||||||
|
fd = open(filename, "w")
|
||||||
|
fd.write(m3u_content)
|
||||||
|
fd.close()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# String utilities
|
||||||
|
|
||||||
|
def escapeStr(uri):
|
||||||
|
return uri.replace(" ", "%20")
|
||||||
|
|
||||||
|
def sec2min(duration):
|
||||||
|
return('%02d:%02d' % (duration / 60, duration % 60))
|
||||||
|
|
||||||
|
# VLC lua utilities
|
||||||
|
|
||||||
|
def sendCommand(host, arg0, arg1, arg2):
|
||||||
|
#conn = http.client.HTTPConnection( host + ":" + str(port), timeout=3 )
|
||||||
|
portl = port
|
||||||
|
# Build request
|
||||||
|
req = "/requests/status.xml"
|
||||||
|
if arg0 == "list" :
|
||||||
|
req = "/requests/playlist.xml"
|
||||||
|
elif arg0 == "rssi":
|
||||||
|
req = "/rssi"
|
||||||
|
portl = cmd_port
|
||||||
|
elif arg0 == "reboot":
|
||||||
|
req = "/reboot"
|
||||||
|
portl = cmd_port
|
||||||
|
elif arg0 == "poweroff":
|
||||||
|
req = "/poweroff"
|
||||||
|
portl = cmd_port
|
||||||
|
elif arg0 != "status" :
|
||||||
|
req = req + "?command=" + cmd[arg0]
|
||||||
|
if arg1 != "null" :
|
||||||
|
if (arg0 == "play") or (arg0 == "delete") or (arg0 == "sort") or (arg0 == "move"):
|
||||||
|
req = req + "&id=" + arg1
|
||||||
|
if (arg0 == "sort") or (arg0 == "move") :
|
||||||
|
# val possible values : id, title, title nodes first, artist, genre, random, duration, title numeric, album
|
||||||
|
# https://github.com/videolan/vlc/blob/3.0.17.4/modules/lua/libs/playlist.c#L353-L362
|
||||||
|
req = req + "&val=" + escapeStr(arg2)
|
||||||
|
elif arg0 == "seek" :
|
||||||
|
req = req + "&val=" + arg1
|
||||||
|
elif (arg0 == "enqueue") or (arg0 == "add") :
|
||||||
|
req = req + "&input=file://" + video_folder + "/" + arg1
|
||||||
|
# Send request
|
||||||
|
conn = http.client.HTTPConnection( host + ":" + str(portl), timeout=3 )
|
||||||
|
try:
|
||||||
|
conn.request( "GET", req, headers={"Authorization":"Basic " + auth} )
|
||||||
|
except:
|
||||||
|
return "Connection to " + host + " was refused on port " + str(portl)
|
||||||
|
resp = conn.getresponse()
|
||||||
|
# Parse response
|
||||||
|
data = resp.read()
|
||||||
|
|
||||||
|
if arg0 == "rssi":
|
||||||
|
if DEBUG:
|
||||||
|
print(data)
|
||||||
|
response_dict = {
|
||||||
|
'host': host,
|
||||||
|
'rssi': str(data, 'UTF-8')
|
||||||
|
}
|
||||||
|
return response_dict
|
||||||
|
|
||||||
|
xml = ET.fromstring(data)
|
||||||
|
|
||||||
|
if arg0 != "list" :
|
||||||
|
meta = xml.findall("./information/category/")
|
||||||
|
if meta:
|
||||||
|
for leaf in meta:
|
||||||
|
if leaf.get("name") == "filename":
|
||||||
|
filename = leaf.text
|
||||||
|
else:
|
||||||
|
filename = "N/A"
|
||||||
|
cur_length = int(xml.find('length').text)
|
||||||
|
cur_time = int(xml.find('time').text)
|
||||||
|
cur_length_fmtd = sec2min(cur_length)
|
||||||
|
cur_time_fmtd = sec2min(cur_time)
|
||||||
|
response_dict = {
|
||||||
|
'host': host,
|
||||||
|
'file': filename,
|
||||||
|
'time': cur_time_fmtd,
|
||||||
|
'leng': cur_length_fmtd,
|
||||||
|
'pos': xml.find('position').text,
|
||||||
|
'loop': xml.find('loop').text,
|
||||||
|
'repeat': xml.find('repeat').text,
|
||||||
|
}
|
||||||
|
return response_dict
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Build M3U file from playlist
|
||||||
|
playlist = []
|
||||||
|
playlist_duration = 0
|
||||||
|
m3u_hdr = "#EXTM3U\n"
|
||||||
|
m3u_prefix = "#EXTINF:"
|
||||||
|
item_list = []
|
||||||
|
if xml.find("./node") and xml.find("./node").get('name') == "Playlist":
|
||||||
|
m3u_content = m3u_hdr
|
||||||
|
playlist = xml.findall("./node/leaf")
|
||||||
|
#item_list = []
|
||||||
|
for item in playlist:
|
||||||
|
m3u_content += m3u_prefix + item.get("duration") + ", " + item.get("name") + "\n" + item.get("uri") + "\n"
|
||||||
|
playlist_duration += int(item.get("duration"))
|
||||||
|
# ~ item_info = item.get("id") + " : " + item.get("name") + " - " + sec2min( int( item.get("duration") ) )
|
||||||
|
item_info = item.get("id") + ";" + item.get("name") + ";" + sec2min( int( item.get("duration") ) )
|
||||||
|
item_list.append(item_info)
|
||||||
|
|
||||||
|
playlist_overview = {
|
||||||
|
"host" : host,
|
||||||
|
"leng" : str(len(playlist)),
|
||||||
|
"duration" : sec2min(playlist_duration),
|
||||||
|
"items" : item_list
|
||||||
|
}
|
||||||
|
return playlist_overview
|
||||||
|
|
||||||
|
status_message = "Idle"
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def main():
|
||||||
|
global hosts
|
||||||
|
status_message = "Searching network for live hosts..."
|
||||||
|
# ~ hosts_available, hosts_unavailable = checkHosts(hosts)
|
||||||
|
templateData = {
|
||||||
|
'hosts' : hosts,
|
||||||
|
'status_message' : status_message
|
||||||
|
}
|
||||||
|
return render_template('main.html', **templateData)
|
||||||
|
|
||||||
|
@app.route("/scan")
|
||||||
|
def scan():
|
||||||
|
global hosts_available, hosts_unavailable
|
||||||
|
hosts_available, hosts_unavailable = checkHosts(hosts)
|
||||||
|
hosts_status = [hosts_available, hosts_unavailable]
|
||||||
|
return hosts_status
|
||||||
|
|
||||||
|
@app.route("/browse")
|
||||||
|
def browse():
|
||||||
|
files = listMediaFiles("../");
|
||||||
|
return files;
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/<host>/<arg0>/", defaults = { "arg1": "null", "arg2": "null" })
|
||||||
|
@app.route("/<host>/<arg0>/<arg1>/", defaults = { "arg2": "null" })
|
||||||
|
@app.route("/<host>/<arg0>/<arg1>/<arg2>")
|
||||||
|
def action(host, arg0, arg1, arg2):
|
||||||
|
status_message = "Idle"
|
||||||
|
if arg0 not in cmd:
|
||||||
|
# ~ return "<p>Wrong command</p>"
|
||||||
|
status_message = "<p>Wrong command</p>"
|
||||||
|
elif host == "all":
|
||||||
|
resp = []
|
||||||
|
for hostl in hosts_available:
|
||||||
|
resp.append( sendCommand(hostl, arg0, arg1, arg2) )
|
||||||
|
status_message = resp
|
||||||
|
elif host not in hosts_available:
|
||||||
|
status_message = "<p>Host is not reachable</p>"
|
||||||
|
else:
|
||||||
|
status_message = sendCommand(host, arg0, arg1, arg2)
|
||||||
|
if DEBUG:
|
||||||
|
print(status_message)
|
||||||
|
return status_message
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
||||||
|
# ~ serve(app, host='127.0.0.1', port=8080)
|
|
@ -0,0 +1,177 @@
|
||||||
|
var scanInterval = 3000;
|
||||||
|
// Bouttons de commande
|
||||||
|
addEventListener("DOMContentLoaded", function() {
|
||||||
|
sendCmd("/scan");
|
||||||
|
sendCmd("/browse");
|
||||||
|
sendCmd("/all/rssi");
|
||||||
|
// Tous les elements avec la classe ".command"
|
||||||
|
var commandButtons = document.querySelectorAll(".command");
|
||||||
|
for (var i=0, l=commandButtons.length; i<l; i++) {
|
||||||
|
var button = commandButtons[i];
|
||||||
|
// Sur un click
|
||||||
|
button.addEventListener("click", function(event) {
|
||||||
|
// On intercepte le signal
|
||||||
|
event.preventDefault();
|
||||||
|
// On recupere la valeur de value="" sur le bouton
|
||||||
|
var clickedButton = event.currentTarget;
|
||||||
|
var command = clickedButton.value;
|
||||||
|
if (( command == "/10.42.0.135/reboot" ) || ( command == "/10.42.0.135/poweroff" )) {
|
||||||
|
if ( !confirm("Êtes vous certain de vouloir effectuer cette action ?") ) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else if ( command == "/scan" ) {
|
||||||
|
document.getElementById("status_all").innerHTML = "Searching network for live hosts...";
|
||||||
|
} else if ( command.indexOf("/sort") > -1 ){
|
||||||
|
if (command.indexOf('/1/') > -1 ) {
|
||||||
|
clickedButton.value = clickedButton.value.replace('/1/','/0/')
|
||||||
|
} else {
|
||||||
|
clickedButton.value = clickedButton.value.replace('/0/','/1/')
|
||||||
|
}
|
||||||
|
} else if ( command.indexOf("/move") > -1 ) {
|
||||||
|
const test_array = [5,6,7,8];
|
||||||
|
for (i=test_array.length, l=0;i>l;i--){
|
||||||
|
console.log(test_array[i-1]);
|
||||||
|
sendCmd("/all/move/" + test_array[i-1] + "/1");
|
||||||
|
|
||||||
|
};
|
||||||
|
//setInterval( sendCmd, scanInterval, "/all/move/16/1");
|
||||||
|
};
|
||||||
|
|
||||||
|
// On envoie la commande en AJAX
|
||||||
|
var request = new XMLHttpRequest();
|
||||||
|
if ( command == "/scan" ) {
|
||||||
|
request.onload = refreshInfos(command);
|
||||||
|
}
|
||||||
|
// On construit la commande
|
||||||
|
request.open("GET", command, true);
|
||||||
|
// et on l'envoie
|
||||||
|
request.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
// Affichage des infos
|
||||||
|
function parseResult(command, infos_array) {
|
||||||
|
//~ var request = new XMLHttpRequest();
|
||||||
|
//~ request.onload = function() {
|
||||||
|
//~ if (request.readyState === request.DONE) {
|
||||||
|
//~ if (request.status === 200) {
|
||||||
|
//~ // responseText is a string, use parse to get an array.
|
||||||
|
//~ const infos_array = JSON.parse(request.responseText);
|
||||||
|
switch (command) {
|
||||||
|
case "/all/status":
|
||||||
|
// Iterate over array
|
||||||
|
for (var i = 0, l=infos_array.length; i<l; i++) {
|
||||||
|
document.getElementById("status_"+infos_array[i].host).innerHTML = "Playing " + infos_array[i].file + " : " + infos_array[i].time + " / " + infos_array[i].leng;
|
||||||
|
if (infos_array[i].loop == "true") { document.getElementById("loop_ind_" + infos_array[i].host).style.backgroundColor = "#0f0"} else {document.getElementById("loop_ind_" + infos_array[i].host).style.backgroundColor = "#f00"};
|
||||||
|
if (infos_array[i].repeat == "true") { document.getElementById("repeat_ind_" + infos_array[i].host).style.backgroundColor = "#0f0"} else {document.getElementById("repeat_ind_" + infos_array[i].host).style.backgroundColor = "#f00"};
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "/all/list":
|
||||||
|
for (var i = 0, l=infos_array.length; i<l; i++) {
|
||||||
|
document.getElementById("playlist_"+infos_array[i].host).innerHTML = infos_array[i].leng + " item(s) in playlist - " + infos_array[i].duration;
|
||||||
|
var items_array = Array.from(infos_array[i].items);
|
||||||
|
var html_table = "<table>" +
|
||||||
|
"<tr>" +
|
||||||
|
"<th>Id</th>" +
|
||||||
|
"<th>Filename</th>" +
|
||||||
|
"<th>Duration</th>" +
|
||||||
|
"</tr>";
|
||||||
|
for (var j = 0, k=items_array.length; j<k; j++) {
|
||||||
|
item_meta = items_array[j].split(';');
|
||||||
|
html_table += "<tr>" +
|
||||||
|
"<td>" + item_meta[0] + "</td>" +
|
||||||
|
"<td>" + item_meta[1] + "</td>" +
|
||||||
|
"<td>" + item_meta[2] + "</td>" +
|
||||||
|
"</tr>" ;
|
||||||
|
}
|
||||||
|
html_table += "</table>";
|
||||||
|
document.getElementById("playlist_"+infos_array[i].host).innerHTML += html_table;
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "/scan":
|
||||||
|
var host_up = infos_array[0];
|
||||||
|
var host_down = infos_array[1];
|
||||||
|
for ( var i=0, l=host_up.length; i<l; i++){
|
||||||
|
document.getElementById(host_up[i]).style.display = 'initial';
|
||||||
|
}
|
||||||
|
for ( var i=0, l=host_down.length; i<l; i++){
|
||||||
|
document.getElementById(host_down[i]).style.display = 'none';
|
||||||
|
}
|
||||||
|
//~ if (host_up.length) {
|
||||||
|
//~ scanInterval = 10000;
|
||||||
|
//~ document.getElementById("status_all").innerHTML = "Scan intarvel set to " + scanInterval;
|
||||||
|
//~ }
|
||||||
|
document.getElementById("status_all").innerHTML = host_up.length + " client(s) found.";
|
||||||
|
break;
|
||||||
|
case "/browse":
|
||||||
|
var html_table = "<table>" +
|
||||||
|
"<tr>" +
|
||||||
|
"<th>Filename</th>" +
|
||||||
|
"<th>Duration</th>" +
|
||||||
|
"</tr>";
|
||||||
|
for (var j = 0, k=infos_array.length; j<k; j++) {
|
||||||
|
html_table += "<tr>" +
|
||||||
|
"<td>" + infos_array[j] + "</td>" +
|
||||||
|
"<td>" + "00:00" + "</td>" +
|
||||||
|
"</tr>" ;
|
||||||
|
}
|
||||||
|
html_table += "</table>";
|
||||||
|
document.getElementById("filelist").innerHTML += html_table;
|
||||||
|
break;
|
||||||
|
case "/all/rssi":
|
||||||
|
var signal_color = 40;
|
||||||
|
var best_rssi = 30;
|
||||||
|
var worst_rssi = 70;
|
||||||
|
for (var j = 0, k=infos_array.length; j<k; j++) {
|
||||||
|
var rssi_norm = Math.ceil( (worst_rssi - parseInt(infos_array[j].rssi) ) / ( worst_rssi - best_rssi ) * 4 );
|
||||||
|
signal_color = (rssi_norm-1) * signal_color;
|
||||||
|
// Reset to grey
|
||||||
|
for (i=0, l=4; i<l;i++) {
|
||||||
|
document.getElementById("wl_"+i).style.backgroundColor = "hsl(0, 0%, 65%)";
|
||||||
|
};
|
||||||
|
// Color it
|
||||||
|
for (i=0, l=rssi_norm; i<l;i++) {
|
||||||
|
document.getElementById("wl_"+i).style.backgroundColor = "hsl(" + signal_color + ", 100%, 50%)";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}; // End switch case
|
||||||
|
//~ };
|
||||||
|
//~ };
|
||||||
|
//~ };
|
||||||
|
//~ // On construit la commande
|
||||||
|
//~ request.open("GET", command, true);
|
||||||
|
//~ // et on l'envoie
|
||||||
|
//~ request.send();
|
||||||
|
};
|
||||||
|
|
||||||
|
function sendCmd(command) {
|
||||||
|
var request = new XMLHttpRequest();
|
||||||
|
request.onload = function() {
|
||||||
|
if (request.readyState === request.DONE) {
|
||||||
|
if (request.status === 200) {
|
||||||
|
// responseText is a string, use parse to get an array.
|
||||||
|
var infos_array = JSON.parse(request.responseText);
|
||||||
|
//console.log(infos_array);
|
||||||
|
parseResult(command, infos_array);
|
||||||
|
//return infos_array;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// On construit la commande
|
||||||
|
request.open("GET", command, true);
|
||||||
|
// et on l'envoie
|
||||||
|
request.send();
|
||||||
|
};
|
||||||
|
setInterval( sendCmd, 500, "/all/status");
|
||||||
|
setInterval( sendCmd, 1000, "/all/list");
|
||||||
|
setInterval( sendCmd, scanInterval, "/scan");
|
||||||
|
setInterval( sendCmd, 10000, "/all/rssi");
|
||||||
|
|
||||||
|
|
||||||
|
/* TODO :
|
||||||
|
* Change scanInterval after first results are in (3 to 30 seconds)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
body {color:#fff;background-color:#666;}
|
||||||
|
h2 {margin:0;}
|
||||||
|
table {background-color: #555;margin:.5em;}
|
||||||
|
td {padding: .2em .5em;}
|
||||||
|
th {background-color: #aaa;}
|
||||||
|
tr:nth-child(2n+1) {background-color: #888;}
|
||||||
|
|
||||||
|
#master_remote {background-color:#222;min-height: 20em;}
|
||||||
|
#master_remote .right_col {background-color:transparent;}
|
||||||
|
.client_container {border-bottom: #bbb solid 4px;display:none;}
|
||||||
|
.client_container .button{}
|
||||||
|
.timeline {height: 3em;background-color: #0f0;margin: 2em 0;}
|
||||||
|
.right_col {width: 78%;display: inline-block;background-color:#888;}
|
||||||
|
.client_container:nth-child(2n+1) {background-color:#444;}
|
||||||
|
.command {margin: 0 !important;}
|
||||||
|
.buttons {width:75%;margin:auto;text-align: center;padding: 2em;}
|
||||||
|
.btn {
|
||||||
|
margin:auto;
|
||||||
|
width:4em;
|
||||||
|
height: 4em;
|
||||||
|
display:inline-block;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
-ms-touch-action: manipulation;
|
||||||
|
touch-action: manipulation;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
background-image: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-right-color: transparent;
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
border-left-color: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.btn_txt {display: block;font-size: small;}
|
||||||
|
/*Left column*/
|
||||||
|
.left_col {width: 20%;display: inline-block;float: left;clear: left;}
|
||||||
|
.left_col button {
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 2em;
|
||||||
|
}
|
||||||
|
.left_col button .btn_txt {display:none;position: absolute;left: 5%;color:#fff;font-weight:bold;font-size: medium;}
|
||||||
|
.left_col button:hover .btn_txt {display:initial;}
|
||||||
|
.col_1 {
|
||||||
|
width: 50%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.col_2 {
|
||||||
|
width: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
clear: right;
|
||||||
|
}
|
||||||
|
.indicator {display:inline-block;background-color: #f00;}
|
||||||
|
.wl_indicator {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #bbb;
|
||||||
|
vertical-align: bottom;
|
||||||
|
margin: 0 1px;
|
||||||
|
padding: 0;
|
||||||
|
height: .5em;
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
#wl_0 {height:.5em;}
|
||||||
|
#wl_1 {height:.65em;}
|
||||||
|
#wl_2 {height:.80em;}
|
||||||
|
#wl_3 {height:.95em;}
|
|
@ -0,0 +1,75 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<title>RPi Web Server</title>
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
|
||||||
|
<!--
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
|
||||||
|
-->
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{url_for('static', filename='style.css')}}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="master_remote">
|
||||||
|
<div class="left_col">
|
||||||
|
<h2>Videopi commander</h2>
|
||||||
|
<p id="status_all">{{status_message}}</p>
|
||||||
|
<p id="filelist">No files.</p>
|
||||||
|
</div>
|
||||||
|
<div class="right_col">
|
||||||
|
<p class="buttons">
|
||||||
|
<button value="/scan" class="command btn btn-block btn-lg btn-default" role="button">🔍<span class="btn_txt">Scan<br/>réseau</span></button>
|
||||||
|
<button value="/all/previous" class="command btn btn-block btn-lg btn-default" role="button">⏮<span class="btn_txt">Préc.</span></button>
|
||||||
|
<button value="/all/play" class="command btn btn-block btn-lg btn-default" role="button">⏵<span class="btn_txt">Lecture</span></button>
|
||||||
|
<button value="/all/pause" class="command btn btn-block btn-lg btn-default" role="button">⏸<span class="btn_txt">Pause</span></button>
|
||||||
|
<button value="/all/stop" class="command btn btn-block btn-lg btn-default" role="button">⏹<span class="btn_txt">Stop</span></button>
|
||||||
|
<button value="/all/next" class="command btn btn-block btn-lg btn-default" role="button">⏭<span class="btn_txt">Suivant</span></button>
|
||||||
|
<button value="/all/repeat" class="command btn btn-block btn-lg btn-default" role="button">🔂<span class="btn_txt">Répéter<br/>élem.</span></button>
|
||||||
|
<button value="/all/loop" class="command btn btn-block btn-lg btn-default" role="button">🔁<span class="btn_txt">Boucler<br/>liste</span></button>
|
||||||
|
<button value="/all/clear" class="command btn btn-block btn-lg btn-default" role="button">X<span class="btn_txt">Vider<br/>listes</span></button>
|
||||||
|
<button value="/all/enqueue/tst.mp4" class="command btn btn-block btn-lg btn-default" role="button">β<span class="btn_txt">tst.mp4</span></button>
|
||||||
|
<button value="/all/enqueue/tst1.mp4" class="command btn btn-block btn-lg btn-default" role="button">β<span class="btn_txt">tst1.mp4</span></button>
|
||||||
|
<button value="/move" class="command btn btn-block btn-lg btn-default" role="button">β<span class="btn_txt">movePl</span></button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% for host in hosts %}
|
||||||
|
<div class="client_container" id="{{ host }}">
|
||||||
|
<div class="left_col">
|
||||||
|
<div class="col_1">
|
||||||
|
<h2>{{ host }}</h2>
|
||||||
|
<button value="/{{host}}/poweroff" class="command btn btn-block btn-lg btn-default" role="button">⏻<span class="btn_txt">Éteindre</span></button>
|
||||||
|
<button value="/{{host}}/reboot" class="command btn btn-block btn-lg btn-default" role="button">↺<span class="btn_txt">Redémarrer</span></button>
|
||||||
|
<p id="status_{{ host }}">{{status_message}}</p>
|
||||||
|
<p id="signal_{{ host }}">
|
||||||
|
<span style="">Signal:</span>
|
||||||
|
<span class="wl_indicator" id="wl_0"></span>
|
||||||
|
<span class="wl_indicator" id="wl_1"></span>
|
||||||
|
<span class="wl_indicator" id="wl_2"></span>
|
||||||
|
<span class="wl_indicator" id="wl_3"></span>
|
||||||
|
</p>
|
||||||
|
<p id="loop_ind_{{ host }}" class="indicator">Loop</p>
|
||||||
|
<p id="repeat_ind_{{ host }}" class="indicator">Repeat</p>
|
||||||
|
</div>
|
||||||
|
<div class="col_2">
|
||||||
|
<p id="playlist_{{ host }}">No files queued.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right_col">
|
||||||
|
<div class="timeline"></div>
|
||||||
|
<p class="buttons">
|
||||||
|
<button value="/{{host}}/previous" class="command btn btn-block btn-lg btn-default" role="button">⏮<span class="btn_txt">Préc.</span></button>
|
||||||
|
<button value="/{{host}}/play" class="command btn btn-block btn-lg btn-default" role="button">⏵<span class="btn_txt">Lecture</span></button>
|
||||||
|
<button value="/{{host}}/pause" class="command btn btn-block btn-lg btn-default" role="button">⏸<span class="btn_txt">Pause</span></button>
|
||||||
|
<button value="/{{host}}/stop" class="command btn btn-block btn-lg btn-default" role="button">⏹<span class="btn_txt">Stop</span></button>
|
||||||
|
<button value="/{{host}}/next" class="command btn btn-block btn-lg btn-default" role="button">⏭<span class="btn_txt">Suivant</span></button>
|
||||||
|
<button value="/{{host}}/repeat" class="command btn btn-block btn-lg btn-default" role="button">🔂<span class="btn_txt">Répéter<br/>élem.</span></button>
|
||||||
|
<button value="/{{host}}/loop" class="command btn btn-block btn-lg btn-default" role="button">🔁<span class="btn_txt">Boucler<br/>liste</span></button>
|
||||||
|
<button value="/{{host}}/clear" class="command btn btn-block btn-lg btn-default" role="button">X<span class="btn_txt">Vider<br/>liste</span></button>
|
||||||
|
<button id="toggle_val_{{host}}}" value="/{{host}}/sort/1/id" class="command btn btn-block btn-lg btn-default" role="button">🔀<span class="btn_txt">Trier<br/>liste</span></button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</body>
|
||||||
|
<script type="text/javascript" src="{{url_for('static', filename='script.js')}}"></script>
|
||||||
|
</html>
|
Loading…
Reference in New Issue