French l10n of app

This commit is contained in:
ABelliqueux 2022-11-03 12:48:12 +01:00
parent 713a1eef79
commit 0ccf5eb40c
8 changed files with 0 additions and 1066 deletions

386
app.py
View File

@ -1,386 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
import sys, os, base64, toml
import http.client, ssl
import xml.etree.ElementTree as ET
from flask import Flask, render_template, request, make_response, jsonify
from waitress import serve
app = Flask(__name__)
# Load config defaults, then look for other config files
app.config.from_file("defaults.toml", load=toml.load)
config_locations = ["./", "~/.", "~/.config/"]
for location in config_locations:
# Optional config files, ~ is expanded to $HOME on *nix, %USERPROFILE% on windows
# ~ app.config.from_file("videopi.toml", load=toml.load, silent=True)
if app.config.from_file(os.path.expanduser( location + "pilpil-server.toml"), load=toml.load, silent=True):
print("Found configuration file in " + os.path.expanduser( location ))
# ~ app.config.from_file(os.path.expanduser("~/.config/videopi.toml"), load=toml.load, silent=True)
###
hosts_available, hosts_unavailable = [],[]
# Map vlc cmds
# See https://github.com/videolan/vlc/blob/1336447566c0190c42a1926464fa1ad2e59adc4f/share/lua/http/requests/README.txt
cmd_player = {
"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",
}
cmd_server = ["blink", "reboot", "poweroff", "rssi"]
# Set configuration
DEBUG = app.config['DEFAULT']['DEBUG']
media_folder_remote = app.config['DEFAULT']['media_folder_remote']
media_folder_local = os.path.expanduser(app.config['DEFAULT']['media_folder_local'])
media_exts = app.config['DEFAULT']['media_exts']
auth = str(base64.b64encode(str(":" + app.config['DEFAULT']['auth']).encode('utf-8')), 'utf-8')
cmd_auth = str(base64.b64encode(str(":" + app.config['DEFAULT']['cmd_auth']).encode('utf-8')), 'utf-8')
hosts = app.config['DEFAULT']['hosts']
port = app.config['DEFAULT']['port']
cmd_port = app.config['DEFAULT']['cmd_port']
useSSL = app.config['DEFAULT']['useSSL']
CAfile = app.config['DEFAULT']['CAfile']
sync_facility = app.config['DEFAULT']['sync_facility']
headers = {"Authorization":"Basic " + auth}
# 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)
if useSSL:
sslcontext = ssl.create_default_context()
# ~ if os.path.exists(CAfile):
# ~ sslcontext.load_verify_locations(cafile=CAfile)
# ~ else:
# Dont validate cert, we juts want to see if host is live
sslcontext.check_hostname = False
sslcontext.verify_mode = ssl.CERT_NONE
s = sslcontext.wrap_socket(s, server_hostname=host_l)
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 connection to " + host_l + ":" + str(port) + ": %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, port):
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):
if os.path.exists(folder):
files = os.listdir(folder);
medias = []
for fd in files:
if len(fd.split('.')) > 1:
if fd.split('.')[1] in media_exts:
medias.append(fd)
return medias
else:
return []
def httpUpload(filename, hostl, trailing_slash=1):
import requests
url = "https://" + hostl + ":" + str(cmd_port) + "/upload"
if not trailing_slash:
filename = "/" + filename
files = { "file":( filename, open( media_folder_local + filename, "rb"), "multipart/form-data") }
if DEBUG:
print(files)
resp = requests.post(url, files=files, headers=headers, verify=CAfile)
if DEBUG:
print(resp.text)
if resp.ok:
return 1
else:
return 0
def syncMediaFolder(media_folder_local, media_folder_remote, hostl, sync_facility=sync_facility):
trailing_slash = 1
# Check for trailing / and add it if missing
if media_folder_local[-1:] != "/":
media_folder_local += "/"
trailing_slash = 0
if media_folder_remote[-1:] != "/":
media_folder_remote += "/"
if sync_facility == "http":
media_list = listMediaFiles(media_folder_local)
transfer_ok = 0
for media in media_list:
transfer_ok += httpUpload(media, hostl, trailing_slash)
return str(transfer_ok) + " files uploaded."
# Check sync utility exists
elif which(sync_facility):
from shutil import whichdb
# Build subprocess arg list accroding to facility
if sync_facility == "rsync":
scrape_str = "total size is "
sync_args = [sync_facility, "-zharm", "--include='*/'"]
for media_type in media_exts:
sync_args.append( "--include='*." + media_type + "'" )
sync_args.extend(["--exclude='*'", media_folder_local, hostl + ":" + media_folder_remote])
if sync_facility == "scp":
media_list = listMediaFiles(media_folder_local)
sync_args = [sync_facility, "-Crp", "-o IdentitiesOnly=yes"]
for media in media_list:
sync_args.append( media_folder_local + media )
sync_args.append( hostl + ":" + media_folder_remote )
sync_proc = subprocess.run( sync_args , capture_output=True)
if len(sync_proc.stdout):
scrape_index = str(sync_proc.stdout).index(scrape_str)+len(scrape_str)
total_size = str(sync_proc.stdout)[ scrape_index: ].split(" ")[0][:-1]
else:
total_size = "N/A";
return total_size
# /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):
portl = port
# Build request
req = "/requests/status.xml"
if arg0 == "list" :
req = "/requests/playlist.xml"
elif arg0 in cmd_server:
req = "/" + str(arg0)
portl = cmd_port
elif arg0 != "status" :
req = req + "?command=" + cmd_player[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://" + media_folder_remote + "/" + arg1
# Send request
if useSSL:
sslcontext = ssl.create_default_context()
if os.path.exists(CAfile):
sslcontext.load_verify_locations(cafile=CAfile)
else:
sslcontext.check_hostname = False
sslcontext.verify_mode = ssl.CERT_NONE
conn = http.client.HTTPSConnection( host + ":" + str(portl), timeout=3, context = sslcontext )
else:
conn = http.client.HTTPConnection( host + ":" + str(portl), timeout=3 )
try:
conn.request( "GET", req, headers = headers )
resp = conn.getresponse()
data = resp.read()
except http.client.HTTPException:
print("Connection to " + host + " timed out")
return "Connection to " + host + " timed out"
except:
return "Error while connecting to " + host + ":" + str(portl)
finally:
conn.close()
# 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)
cur_id = int(xml.find('currentplid').text)
cur_pos = xml.find('position').text
cur_loop = xml.find('loop').text
cur_repeat = xml.find('repeat').text
response_dict = {
'host': host,
'file': filename,
'time': cur_time_fmtd,
'leng': cur_length_fmtd,
'pos': cur_pos,
'loop': cur_loop,
'repeat': cur_repeat,
# ~ 'pos': xml.find('position').text,
# ~ 'loop': xml.find('loop').text,
# ~ 'repeat': xml.find('repeat').text,
'id': cur_id,
}
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") ) ) + ";"
if "current" in item.keys():
item_info += item.get("current")
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(media_folder_local)
return files;
@app.route("/sync/<host>")
def sync(host):
if host == "all":
for hostl in hosts_available:
size = syncMediaFolder(media_folder_local, media_folder_remote, hostl)
else:
size = syncMediaFolder(media_folder_local, media_folder_remote, host)
return size;
@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_player) and (arg0 not in cmd_server):
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)

View File

@ -1,79 +0,0 @@
## 0.1 : 2022-07-19-videopi.img.xz
md5sum : 7e80ede8ac4eed8b8088a3b075bdc1f2
sha256 : 03de0272c71bd4614678b05c076d0e77df3f49039ad357ef9152374c748e7f1c
* VLC installed, H264 1080p playback ok, wifi ok, remote control via telnet script
* Run `ssh-keygen -A` at first boot for the ssh server to work
## 0.2 : 2022-09-24-videopi.img.xz
md5sum : f859f269c44f614e22e4fe601c3bb134
sha256 : b6fd8ef4eb726d4ce7d196b9aebf910f32327ecd43f0d78140b8647d328ded22
* Switch VLC to use http lua control
* Add systemd unit for running VLC on startup
* Add RTL8821CU driver for rpi 1/3
* Boot is now totally silent (blank screen)
* Disable Bluetooth
## 0.3 : 2022-10-09-videopi.img.xz
md5 : 8e5e5b474af47519785d5a4696db04e2
sha256 : 0fe3fe76d0e56e445124fa20646fa8b3d8c59568786b3ebc8a96d83d92f203e3
* Add rtl8192eu driver for rpi1/3
* Add http server for custom commands : reboot, shutdown, wifi signal, led blinking
* Add vlc.playlist.move method to VLC http lua (httprequests.luac)
* Add playlist to Webgui
* config file parsing ( look for 'videopi.toml' in ./, ~/., ~/.config/)
* Add VLC/waitress systemd units for automatic startup
* Use nginx reverse proxy + SSL between server and clients ( https://medium.com/@antelle/how-to-generate-a-self-signed-ssl-certificate-for-an-ip-address-f0dd8dddf754 )
* Webgui beautifying
## 0.4 : 2022-10-21-videopi.img.xz
md5 : dee7af70135994169cab4f073ee51905
sha256 : ec3e17fc9b41f8c5181484e9866be2d1d92cab8403210e3d22f4f689edd4cfde
* Switch to rpi os Bullseye
* Switch to user 'pil', pw 'pilpoil'
* client config file parsing ( look for 'pilpil-client.toml' in ./, ~/., ~/.config/)
* Add media folder sync (scp, rsync, http upload)
* Install script ; Wifi setup, generate/install SSH keys/ nginx SSL cert/key fore each host, change hostname, static IPs
## 0.5 : 2022-10-XX-videopi.img.xz
md5 :
sha256 :
* Add rt8821cu driver back
* Use safe overclocking settings for rpi1, 3, 4, better memory split
* Add blink function to pilpil
# FS checklist
* /etc/dhcpcd.conf
* /etc/ssh/sshd_config
* ~/.ssh/authorized_keys
* ~/Videos/*
* /etc/hostname, /etc/hosts
* /etc/ssl/private/nginx-selfsigned.key
* /etc/ssl/certs/nginx-selfsigned.crt
* /etc/wpa_supplicant/wpa_supplicant.conf
* dd if=/dev/zero of=/home/pil/Videos/remove_me bs=4MB count=100
# DOING NEXT :
* ~ Test with several rpis
* Define http auth secret at setup
# DONE :
* Increase live host scan when host first found
# OTHER:
* get_client_rssi.sh on server
# TODO :
* FR localisation
* GUI : Btn hover/press ?
* ? Scripts hotspot linux/win/mac
* ? Config sync
* ! Remove git personal details/resolv.conf, remove authorized_keys, ssh config, clean home, re-enable ssh pw login
* ~ Doc

View File

@ -1,16 +0,0 @@
[DEFAULT]
DEBUG = 0
useSSL = false
CAfile = "selfCA.crt"
# scp, rsync, http
sync_facility = "http"
media_folder_local = "~/Videos"
media_folder_remote = "~/Videos"
media_exts = []
auth = "secret"
cmd_auth = "secret"
hosts = []
# VLC http LUA port
port = 0
# Clients cmd port
cmd_port = 0

View File

@ -1,17 +0,0 @@
[DEFAULT]
DEBUG = 0
useSSL = true
CAfile = "selfCA.crt"
# scp, rsync, http
sync_facility = "http"
media_folder_local = "../medias"
media_folder_remote = "/home/pil/Videos"
media_exts = ["mp4", "avi", "mkv"]
auth = "secret"
# OnNlY3JldA==
cmd_auth = "secret"
hosts = ["10.42.0.10", "10.42.0.11"]
# VLC http LUA port
port = 8887
# Clients cmd port
cmd_port = 8888

View File

@ -1,3 +0,0 @@
flask
waitress
toml

View File

@ -1,326 +0,0 @@
// Timeline drag and drop
const tl_cont_attr = {id:"tl_cont", ondrop: "drop(event, this)", ondragover:"allowDrop(event)"};
const tl_drag_attr = {id:"tl_drag", draggable:"true", ondragstart:"drag(event, this)"};
// Config
var timeline_color_cursor = "#FF8839";
var timeline_color_bg = "#2EB8E6";
var src_id = "";
var medias_status = {};
function updatePlaylist(){
var new_list = [];
var media_count = document.getElementById("timeline").children.length;
for (i=media_count,l=0;i>l;i--) {
//~ new_list.push(document.getElementById("timeline").children[i].children[0].getAttribute("media_id"));
toMove = document.getElementById("timeline").children[i-1].children[0].getAttribute("media_id");
console.log(toMove);
sendCmd("/all/move/" + toMove + "/1");
}
}
function findTargetIndex(element, index, array, thisArg){
if (element == this) {
return index+1;
}
return 0;
};
function moveElements(target) {
var elem_list = Array.from(src_id.parentElement.children);
var elem_list_slice = elem_list;
var source_idx = elem_list.findIndex(findTargetIndex, src_id);
var target_idx = elem_list.findIndex(findTargetIndex, target);
var idx;
if (source_idx < target_idx) {
elem_list_slice = elem_list.slice(source_idx+1, target_idx+1);
idx = source_idx;
} else {
elem_list_slice = elem_list.slice(target_idx, source_idx );
idx = target_idx+1;
}
for (i=0, l=elem_list_slice.length; i<l;i++) {
elem_list[idx+i].appendChild(elem_list_slice[i].children[0]);
}
return target;
};
function allowDrop(ev) {
ev.preventDefault();
drop_id = ev.dataTransfer.getData("text");
};
function drag(ev, source) {
src_id = ev.target.parentElement;
ev.dataTransfer.setData("text", ev.target.id);
};
function drop(ev, target) {
ev.preventDefault();
var data = ev.dataTransfer.getData("text");
if (src_id.id != target.id) {
dropTarget = moveElements(target);
if (dropTarget) {
dropTarget.appendChild(document.getElementById(data));
updatePlaylist();
}
}
};
function adjustTl() {
var child = document.getElementById('timeline').children;
var divWidth = 100 / child.length;
for (i=0, l=child.length;i<l;i++) {
child[i].style.width= divWidth + "%";
}
};
function addAttr(id, attr, val , child=-1) {
var elem = document.getElementById(id);
if (child>-1){
elem = elem.children[child];
}
var att = document.createAttribute(attr);
att.value = val;
elem.setAttributeNode(att);
};
function addElement(type, attr, meta = 0, j = 0){
var elem = document.createElement(type);
var keys_array = Object.keys(attr);
for (i=0, l=keys_array.length;i<l;i++) {
var att = document.createAttribute(keys_array[i]);
if(!i){
att.value = Object.values(attr)[i]+j;
} else {
att.value = Object.values(attr)[i];
}
elem.setAttributeNode(att);
}
// Set playlist id attribute
if (meta) {
att = document.createAttribute("media_id");
att.value = meta[0];
elem.setAttributeNode(att);
}
// Get filename
elem.innerText = meta[1];
return elem;
};
var scanInterval = 3000;
// Bouttons de commande
addEventListener("DOMContentLoaded", function() {
sendCmd("/scan");
sendCmd("/browse");
sendCmd("/all/rssi");
sendCmd("/all/list");
adjustTl();
// 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.indexOf("/reboot" ) > -1 || command.indexOf("/poweroff") > -1 ) {
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 = [21,19,20];
for (i=test_array.length, l=0;i>l;i--){
console.log(test_array[i-1]);
sendCmd("/all/move/" + test_array[i-1] + "/1");
};
sendCmd("/all/list");
//setInterval( sendCmd, scanInterval, "/all/move/16/1");
};
// On envoie la commande en AJAX
var request = new XMLHttpRequest();
if ( command == "/scan" ) {
request.onload = sendCmd(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) {
switch (command) {
case "/all/status":
// Iterate over array
for (var i = 0, l=infos_array.length; i<l; i++) {
// Get filename, time/length
document.getElementById("status_"+infos_array[i].host).innerHTML = infos_array[i].file + " <br/> " + infos_array[i].time + " / " + infos_array[i].leng;
medias_status[infos_array[i].id] = infos_array[i].pos;
// Find currently playing element
//~ var pl_length = document.getElementById("timeline").getAttribute("length");
//~ for (j=0,k=pl_length;j<k;j++){
//~ if (document.getElementById("timeline").children[j].children[0].getAttribute('media_id') == infos_array[i].id ) {
//~ addAttr(document.getElementById("timeline").children[j].children[0].id, "pos", infos_array[i].pos);
//~ }
//~ };
//~ document.getElementById("timeline").children[0].children[0].hasAttribute('media_id')
// Toggle loop indicator
if (infos_array[i].loop == "true") {
document.getElementById("loop_ind_" + infos_array[i].host).style.backgroundColor = "#78E738"
} else {
document.getElementById("loop_ind_" + infos_array[i].host).style.backgroundColor = "#A42000"
};
// Toggle repeat indicator
if (infos_array[i].repeat == "true") {
document.getElementById("repeat_ind_" + infos_array[i].host).style.backgroundColor = "#78E738"
} else {
document.getElementById("repeat_ind_" + infos_array[i].host).style.backgroundColor = "#A42000"
};
};
break;
case "/all/list":
for (var i = 0, l=infos_array.length; i<l; i++) {
// Fill Playlist infos
document.getElementById("playlist_"+infos_array[i].host).innerHTML = infos_array[i].leng + " item(s) in playlist - " + infos_array[i].duration;
// Build html table and timeline
var items_array = Array.from(infos_array[i].items);
//console.log(items_array.length);
if (items_array.length == 0){
var child_list = Array.from(document.getElementById("timeline").children);
for(i=0,l=child_list.length;i<l;i++){
document.getElementById("timeline").removeChild(child_list[i]);
};
break;
}
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++) {
// Table
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>" ;
// Timeline
var child_node = addElement("div", tl_drag_attr, item_meta, j);
var len = document.getElementById("timeline").children.length;
addAttr("timeline", "length", len);
if ( len < items_array.length ) {
document.getElementById("timeline").appendChild( addElement("div", tl_cont_attr, 0, len) );
}
document.getElementById(tl_cont_attr.id + j).replaceChildren(child_node);
// Adjust elements width
adjustTl();
// Highlight currently playing element
if (item_meta[3] != ""){
document.getElementById(tl_cont_attr.id + j).children[0].style.borderBottom = "4px solid " + timeline_color_cursor;
document.getElementById(tl_cont_attr.id + j).children[0].style.fontWeight = "bold";
var pos = medias_status[item_meta[0]] * 100;
//~ pos = pos.toPrecision(2);
var pos1 = pos-1 + "%";
pos = pos + "%";
//console.log( "linear-gradient(90deg," + timeline_color2 + " " + pos1 + ", " + timeline_color1 + " " + pos + ", " + timeline_color2 + " " + pos + ")" );
document.getElementById(tl_cont_attr.id + j).children[0].style.background = "linear-gradient(90deg," + timeline_color_bg + " " + pos1 + ", " + timeline_color_cursor + " " + pos + ", " + timeline_color_bg + " " + pos + ")";
}
}
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 = 'block';
}
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
};
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, 20000, "/all/rssi");

View File

@ -1,155 +0,0 @@
body {color:#fff;background-color:#666;margin:0;}
div {box-sizing: border-box;}
h2 {margin:0;}
table {background-color: #555;margin:.5em;max-width:100%;font-size:.9em}
td {padding: 0;}
th {background-color: #aaa;}
tr:nth-child(2n+1) {background-color: #888;}
* {box-sizing: border-box;}
.btn-group-lg > .btn, .btn-lg {
padding: 10px 16px;
font-size: 18px;
line-height: 1.3333333;
border-radius: 6px;
}
#master_remote {
background: linear-gradient(0deg, #222 10%, #444 80%);
min-height: 20em;
}
#master_remote .right_col {background-color:transparent;}
.client_container {
border-bottom: #222 solid 1px;
display:none;
background: linear-gradient(0deg, #999 10%, #666 80%);
}
.client_container .button{}
/*
.timeline {height: 3em;background-color: #0f0;margin: 2em 0;}
*/
#timeline {
height: 75px;
width:100%;
margin-top:1em;
}
[id^="tl_cont"] {
float: left;
width: 10%;
height: 75px;
margin: 0;
padding: 0;
border: 1px solid #aaaaaa;
}
[id^="tl_drag"] {
cursor: grab;
text-align:center;
height:75px;
line-height: 75px;
width:100%;
background-color:#1F7B99;
}
[id^="tl_cont"]:nth-child(2n+1) [id^="tl_drag"] {
background-color:#255E70;
}
.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:3em;
height: 4em;
display:inline-block;
padding: 0;
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;
border: 1px solid transparent;
border-radius: 4px;
background: linear-gradient(0deg, #b9b9b9 10%, #f9f9f9 80%);
}
.btn:hover {
background:#C8FFBD;
box-shadow: 0 0 10px #A5FF9F;
}
.btn:active {
background: #91FF7C;
box-shadow: 0 0 18px #91FF7C;
}
.btn_txt {display: block;font-size: small;}
/*Right column*/
.right_col {width: 79.9%;display: inline-block;}
/*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;
color: #fff;
font-weight: bold;
font-size: medium;
background-color: #ff3030;
border: 1px solid #fff;
padding: 0 .5%;
width: 100px;
position: absolute;
}
.left_col button:hover .btn_txt {display:block;}
.col_1 {
width: 40%;
float: left;
padding: 3% 0 0 5%;
}
}
.col_2 {
width: 60%;
display: inline-block;
clear: right;
}
.indicator {
display:inline-block;
background-color: #C32600;
margin: 0 0 0 5%;
padding: 0.3em;
}
.wl_indicator {
display: inline-block;
background-color: #bbb;
vertical-align: bottom;
margin: 0 1px;
padding: 0;
height: .5em;
width: 5%;
}
#wl_0 {height:.5em;}
#wl_1 {height:.65em;}
#wl_2 {height:.80em;}
#wl_3 {height:.95em;}
@media screen and (max-width: 800px) {
table {margin:0}
td {padding:0;}
.btn-group-lg > .btn, .btn-lg {
font-size:14px;
}
.left_col {width: 30%;}
.right_col {width: 69.9%;}
.col_2 {
overflow: scroll;
font-size: .9em;
}
}

View File

@ -1,84 +0,0 @@
<!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">&#x1f50d;<span class="btn_txt">Scan<br/>r&eacute;seau</span></button>
<button value="/all/previous" class="command btn btn-block btn-lg btn-default" role="button">&#x23ee;<span class="btn_txt">Pr&eacute;c.</span></button>
<button value="/all/play" class="command btn btn-block btn-lg btn-default" role="button">&#x23f5;<span class="btn_txt">Lecture</span></button>
<button value="/all/pause" class="command btn btn-block btn-lg btn-default" role="button">&#x23f8;<span class="btn_txt">Pause</span></button>
<button value="/all/stop" class="command btn btn-block btn-lg btn-default" role="button">&#x23f9;<span class="btn_txt">Stop</span></button>
<button value="/all/next" class="command btn btn-block btn-lg btn-default" role="button">&#x23ed;<span class="btn_txt">Suivant</span></button>
<button value="/all/repeat" class="command btn btn-block btn-lg btn-default" role="button">&#x1f502;<span class="btn_txt">R&eacute;p&eacute;ter<br/>&eacute;lem.</span></button>
<button value="/all/loop" class="command btn btn-block btn-lg btn-default" role="button">&#x1f501;<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">&#x3b2;<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">&#x3b2;<span class="btn_txt">tst1.mp4</span></button>
<button value="/all/enqueue/sangliers_1080.mp4" class="command btn btn-block btn-lg btn-default" role="button">&#x3b2;<span class="btn_txt">sangli.mp4</span></button>
<button value="/all/move/0/1" class="command btn btn-block btn-lg btn-default" role="button">&#x3b2;<span class="btn_txt">movePl</span></button>
<button value="/sync/all" class="command btn btn-block btn-lg btn-default" role="button">&#x21ad;<span class="btn_txt">Sync</span></button>
</p>
</div>
</div>
{% for host in hosts %}
<div class="client_container" id="{{ host }}">
<div class="left_col">
<h2>{{ host }}</h2>
<div class="col_1">
<button value="/{{host}}/poweroff" class="command btn btn-block btn-lg btn-default" role="button">&#x23fb;<span class="btn_txt">&Eacute;teindre</span></button>
<button value="/{{host}}/reboot" class="command btn btn-block btn-lg btn-default" role="button">&#x21BA;<span class="btn_txt">Red&eacute;marrer</span></button>
<button value="/{{host}}/blink" class="command btn btn-block btn-lg btn-default" role="button">&#x1F4A1;<span class="btn_txt">Blink</span></button>
<p id="status_{{ host }}">{{status_message}}</p>
<p id="signal_{{ host }}">
<span style="">Link:</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 id="timeline">
<!--
<div id="tl_contX"></div>
-->
</div>
<div class="buttons">
<button value="/{{host}}/previous" class="command btn btn-block btn-lg btn-default" role="button">&#x23ee;<span class="btn_txt">Pr&eacute;c.</span></button>
<button value="/{{host}}/play" class="command btn btn-block btn-lg btn-default" role="button">&#x23f5;<span class="btn_txt">Lecture</span></button>
<button value="/{{host}}/pause" class="command btn btn-block btn-lg btn-default" role="button">&#x23f8;<span class="btn_txt">Pause</span></button>
<button value="/{{host}}/stop" class="command btn btn-block btn-lg btn-default" role="button">&#x23f9;<span class="btn_txt">Stop</span></button>
<button value="/{{host}}/next" class="command btn btn-block btn-lg btn-default" role="button">&#x23ed;<span class="btn_txt">Suivant</span></button>
<button value="/{{host}}/repeat" class="command btn btn-block btn-lg btn-default" role="button">&#x1f502;<span class="btn_txt">R&eacute;p&eacute;ter<br/>&eacute;lem.</span></button>
<button value="/{{host}}/loop" class="command btn btn-block btn-lg btn-default" role="button">&#x1f501;<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">&#x1f500;<span class="btn_txt">Trier<br/>liste</span></button>
</div>
</div>
</div>
{% endfor %}
</body>
<script type="text/javascript" src="{{url_for('static', filename='script.js')}}"></script>
</html>