webgui/server: upload dialog/status

This commit is contained in:
ABelliqueux 2022-12-12 16:56:33 +01:00
parent 9012b5240f
commit 0d2eee5f25
5 changed files with 290 additions and 60 deletions

103
app.py
View File

@ -99,7 +99,8 @@ cmd_server = [
]
current_upload = {
# status : idle = 0, uploading = 1
# status : idle = 0, uploading = 1, done = -1
"host": -1,
"status": 0,
"progress": -1,
"filename": -1,
@ -110,6 +111,8 @@ current_upload = {
"transferred_percent": 0
}
stop_upload_flag = 0
# Configuration
debug = app.config['DEFAULT']['debug']
pi_user = app.config['DEFAULT']['pi_user']
@ -245,15 +248,49 @@ def list_media_files(folder):
return []
def reset_current_upload():
global current_upload
current_upload = {
# status : idle = 0, uploading = 1, done = -1
"host": -1,
"status": 0,
"progress": -1,
"filename": -1,
"size": -1,
"total_size": -1,
"total_count": 0,
"transferred_size": 0,
"transferred_percent": 0
}
def stop_upload():
if sync_facility == "http":
# ~ global stop_upload_flag
global current_upload
current_upload["status"] = 0
# ~ stop_upload_flag = 1
reset_current_upload()
return current_upload
elif sync_facility == "rsync":
# TODO : This won't work in windows...
subprocess.run(["pkill", "rsync"])
else:
subprocess.run(["pkill", "scp"])
return _("Interrupting upload...")
def sync_media_folder(media_folder_local, media_folder_remote, host_local, port, sync_facility=sync_facility):
'''
Sync media_folder_local with media_folder_remote using sync_facility
'''
# TODO : first send lift of files to check if they exist and if upload needed
global current_upload
global stop_upload_flag
total_size = 0
# Using http_upload
if sync_facility == "http":
current_upload["host"] = host_local
media_list = list_media_files(media_folder_local)
media_list = get_upload_candidate_list(host_local, port, media_list)
if debug:
@ -266,19 +303,29 @@ def sync_media_folder(media_folder_local, media_folder_remote, host_local, port,
total_size += int(media["size"]) / 1024 / 1024
current_upload["total_size"] = round(total_size)
for media in media_list:
current_media_size = int(media["size"]) / 1024 / 1024
current_upload["filename"] = media["filename"].strip("/")
current_upload["progress"] = media_count
current_upload["size"] = round(current_media_size)
if debug:
print("Upload candidate : " + str(media))
if HTTP_upload(media, host_local, port):
# ~ if not stop_upload_flag:
if current_upload["status"] > 0:
current_media_size = int(media["size"]) / 1024 / 1024
current_upload["filename"] = media["filename"].strip("/")
current_upload["progress"] = media_count
current_upload["size"] = round(current_media_size)
if debug:
print("File size: " + str(round(current_media_size)))
media_count += 1
current_upload["transferred_size"] += round(current_media_size)
current_upload["transferred_percent"] += round((100 / total_size) * current_media_size)
print("Upload candidate : " + str(media))
if HTTP_upload(media, host_local, port):
if debug:
print("File size: " + str(round(current_media_size)))
media_count += 1
current_upload["transferred_size"] += round(current_media_size)
current_upload["transferred_percent"] += round((100 / total_size) * current_media_size)
else:
# Upload interrupted
return _("Upload interrupted")
# ~ stop_upload_flag = 0
# ~ reset_current_upload() # host becomes -1
reset_current_upload()
current_upload["status"] = -1
# Using system cmd
# TODO : fill current_upload with some values from rsync/scp
elif which(sync_facility):
# Build subprocess arg list accroding to sync_facility
# Using Rsync
@ -301,7 +348,7 @@ def sync_media_folder(media_folder_local, media_folder_remote, host_local, port,
total_size = str(sync_proc.stdout)[scrape_index:].split(" ")[0][:-1]
if debug:
print("Transferred size: " + str(round(total_size)))
return total_size
return current_upload
def get_meta_data(host, xml_data, request_="status", m3u_=0):
@ -417,6 +464,7 @@ def send_pilpil_command(host, arg0, arg1, arg2):
Builds a pilpil request according to args, send it and return parsed result.
'''
port_ = vlc_port
arg0_values = ["play", "delete", "sort", "move"]
# Build request
#
# Default request
@ -440,7 +488,8 @@ def send_pilpil_command(host, arg0, arg1, arg2):
# Build request for VLC command
HTTP_request = HTTP_request + "?command=" + cmd_player[arg0]
if arg1 != "null":
if (arg0 == "play") or (arg0 == "delete") or (arg0 == "sort") or (arg0 == "move"):
# ~ if (arg0 == "play") or (arg0 == "delete") or (arg0 == "sort") or (arg0 == "move"):
if arg0 in arg0_values:
# Add 'id' url parameter
HTTP_request = HTTP_request + "&id=" + arg1
if (arg0 == "sort") or (arg0 == "move"):
@ -543,18 +592,26 @@ def browse():
return list_local_media_files(media_folder_local)
@app.route("/sync/<host>")
def sync(host):
# TODO: Add feedback for transfer in GUI
size = 0
if host == "status":
@app.route("/sync/<host>/", defaults={"arg0": "null"})
@app.route("/sync/<host>/<arg0>/")
def sync(host, arg0):
# TODO: move to action() with url /host/sync/...
global current_upload
upload_ok = 0
if arg0 == "status":
return current_upload
if arg0 == "stop":
return stop_upload()
elif host == "all":
for hostl in hosts_available:
size += sync_media_folder(media_folder_local, media_folder_remote_expanded, hostl, cmd_port)
reset_current_upload()
return sync_media_folder(media_folder_local, media_folder_remote_expanded, host, cmd_port)
# TODO : figure it out for multiple hosts
# ~ for hostl in hosts_available:
# ~ sync_media_folder(media_folder_local, media_folder_remote_expanded, hostl, cmd_port)
else:
size = sync_media_folder(media_folder_local, media_folder_remote_expanded, host, cmd_port)
return str(size)
reset_current_upload()
return sync_media_folder(media_folder_local, media_folder_remote_expanded, host, cmd_port)
return current_upload
@app.route("/<host>/<arg0>/", defaults={"arg1": "null", "arg2": "null"})

View File

@ -70,10 +70,14 @@ sha256 : 401359a84c6d60902c05602bd52fae70f0b2ecac36d550b52d14e1e3230854a6
# DOING NEXT :
* webgui: remove file from timeline (drag&drop to bin?)
* webgui/server : interrupt current download ?
# DONE :
* webgui: remove file from timeline (drag&drop to bin?) (check bugs)
* webgui: upload progress dialog
* server : file upload : only send new files, get upload status (/sync/status)
* webgui: l10n html + js
* webgui: Add remote file enqueuing
* webgui: Fix timeline UI
@ -84,8 +88,6 @@ sha256 : 401359a84c6d60902c05602bd52fae70f0b2ecac36d550b52d14e1e3230854a6
# TODO :
* webgui: remove file from timeline (drag&drop to bin?)
* webgui: file sync UI freeze/status/progress bar
* ~ Test with several rpis
* ? Scripts hotspot linux : nmcli
win : https://github.com/JamesCullum/Windows-Hotspot

View File

@ -3,7 +3,7 @@ const DEBUG = 0;
const CMD_PORT = "8888";
const TIMELINE_COLOR_CURSOR = "#FF8839";
const TIMELINE_COLOR_BG = "#2EB8E600";
const DEFAULT_LOCALE = "en"
const DEFAULT_LOCALE = "en";
// t9n
let t9n =
{ fr : {
@ -11,27 +11,37 @@ let t9n =
confirmMessage : "Êtes vous certain de vouloir effectuer cette action ?",
filename : "Nom",
duration : "Durée",
sync : "Transfert des fichiers..."
size : "Taille",
sync : "Transfert des fichiers..." ,
size_unit : " Mio",
of : " de ",
upload_sent_count_msg : " &eacute;l&eacute;ments envoy&eacute;s",
},
en : {
statusDefault : "Searching network for live hosts...",
confirmMessage : "Are you sure?",
filename : "Filename",
duration : "Duration",
sync : "Syncing files..."
size : "Size",
sync : "Syncing files...",
size_unit : " MB",
of : " of ",
upload_sent_count_msg : " elements transferred.",
}
}
};
// Timeline drag and drop elements default attributes
const tl_cont_attr = {id:"tl_cont", ondrop: "drop(event, this)", ondragover:"allow_drop(event)"};
const tl_drag_attr = {id:"tl_drag", draggable:"true", ondragstart:"drag(event, this)"};
const tl_cont_attr = {"id":"tl_cont", "ondrop": "drop(event, this)", "ondragover":"allow_drop(event)"};
const tl_drag_attr = {"id":"tl_drag", "draggable":"true", "ondragstart":"drag(event, this)"};
// Global object
window.currentUser = {
scan_interval : 3000,
status_all : t9n[LOCALE].statusDefault,
medias_status : {},
freeze_timeline_update : 0,
last_ul_host : 0,
};
function sleep(ms) {
let delay = new Promise(function(resolve) {
setTimeout(resolve, ms);
@ -59,6 +69,8 @@ async function update_sort_VLC_playlist(host) {
// Un-freeze timeline update flag
currentUser.freeze_timeline_update = 0;
}
async function update_delete_VLC_playlist(host, delete_element_id) {
let delete_media = document.getElementById(delete_element_id);
let delete_media_cont = delete_media.parentElement;
@ -67,7 +79,7 @@ async function update_delete_VLC_playlist(host, delete_element_id) {
send_ajax_cmd("/" + host + "/delete/" + delete_media_id);
await sleep(90);
adjust_timeline();
currentUser.freeze_timeline_update = 0
currentUser.freeze_timeline_update = 0;
}
function find_target_index(element, index) {
@ -189,11 +201,11 @@ function add_HTML_attr(id, attribute, val , child=-1) {
}
function add_HTML_element(type, attribute, meta=0, uid=0){
function add_HTML_element(type, attribute, meta=0, uid=-1){
// Add HTML element with type 'type' and attributes 'attribute'.
// 'attribute' should be a javascript object containing HTML attributes keys/values
// 'attribute' should be a javascript object containing HTML attributes keys/values to be applied to the new element
// 'meta' should be an array
// 'uid' is used to make the HTML id attribute unique
// 'uid' is used to make the HTML id attribute unique. If not set, this will use the loop's iteration count i
//
let HTML_element = document.createElement(type);
let HTML_attributes = Object.keys(attribute);
@ -201,7 +213,7 @@ function add_HTML_element(type, attribute, meta=0, uid=0){
let new_attribute = document.createAttribute(HTML_attributes[i]);
if(HTML_attributes[i] == "id") {
// HTML id needs to be unique
new_attribute.value = Object.values(attribute)[i] + uid;
new_attribute.value = Object.values(attribute)[i] + (uid != -1 ? uid : i);
} else {
new_attribute.value = Object.values(attribute)[i];
}
@ -212,9 +224,9 @@ function add_HTML_element(type, attribute, meta=0, uid=0){
let media_attribute = document.createAttribute("media_id");
media_attribute.value = meta[0];
HTML_element.setAttributeNode(media_attribute);
// Set filename as inner text
HTML_element.innerText = meta[1];
}
// Set filename as inner text
HTML_element.innerText = meta[1];
return HTML_element;
}
@ -248,7 +260,7 @@ function update_status(infos_array_element) {
media_element.style.backgroundImage = media_cssgrad_rule;
media_element.style.borderBottom = "4px solid " + TIMELINE_COLOR_CURSOR;
} else {
let media_url_str = "url(https://" + infos_array_element.host + ":" + CMD_PORT + "/thumb/" + media_element.innerText + ")"
let media_url_str = "url(https://" + infos_array_element.host + ":" + CMD_PORT + "/thumb/" + media_element.innerText + ")";
media_element.style.backgroundImage = media_url_str;
media_element.style.borderBottom = "None";
}
@ -286,7 +298,7 @@ function update_list(infos_array_element){
}
document.getElementById(tl_cont_attr.id + j).replaceChildren(child_node);
let media_name = document.getElementById(tl_cont_attr.id + j).children[0].innerText;
let media_url_str = "url(https://" + host + ":" + CMD_PORT + "/thumb/" + media_name + ")"
let media_url_str = "url(https://" + host + ":" + CMD_PORT + "/thumb/" + media_name + ")";
document.getElementById(tl_cont_attr.id + j).children[0].style.backgroundImage = media_url_str;
// Adjust elements width
adjust_timeline();
@ -388,6 +400,92 @@ function update_host_list(infos_array){
}
function display_upload_status(command) {
// TODO : First this should set the HTMl with empty values, then call update_upload...
let host = command.split("/")[2];
let container_element = document.getElementById(host);
let siblings = container_element.children;
// Upload dialog container / background
let upload_dialog_cont_attributes = {"id":"ul_dialog_cont_", "class": "upload_dialog_cont"};
let upload_dialog_cont_element = add_HTML_element("div", upload_dialog_cont_attributes, 0, host);
let ul_cont_exists = document.getElementById(upload_dialog_cont_attributes["id"] + host);
// Upload dialog
//~ let upload_dialog_attributes = {"id":"ul_dialog_", "class": "upload_dialog"};
//~ let upload_dialog_element = add_HTML_element("div", upload_dialog_attributes, 0, host);
//~ let ul_dialog_exists = document.getElementById(upload_dialog_cont_attributes["id"] + host);
let upload_status_table = `
<table>
<tbody>
<tr>
<th colspan="2" id="ul_status_title_${host}">${t9n[LOCALE].sync}</th>
</tr>
<tr>
<td id="ul_status_progress_cnt_${host}"></td>
<td id="ul_status_progress_size_${host}"></td>
</tr>
<tr>
<td id="ul_status_filename_${host}"></td>
<td id="ul_status_filesize_${host}"></td>
</tr>
</tbody>
</table>
`
let upload_dialog_HTML = `
<div id='ul_dialog_${host}' class='upload_dialog'>
<div id="ul_status_${host}" class="upload_status"></div>
<div id="ul_progress_${host}" class="progress_bar"></div>
<button id="ul_stop_btn_${host}" value="/sync/${host}/stop" class="command btn btn-block btn-lg btn-default" role="button">&#x23f9;<span class="btn_txt">Stop</span></button>
</div>
`
if ( ul_cont_exists != undefined) {
container_element.removeChild(ul_cont_exists);
}
container_element.insertBefore(upload_dialog_cont_element, siblings[0]);
document.getElementById(upload_dialog_cont_attributes["id"] + host).innerHTML = upload_dialog_HTML;
document.getElementById("ul_stop_btn_" + host).addEventListener("click", send_btn_cmd, false);
document.getElementById("ul_status_" + host).innerHTML = upload_status_table;
}
function destroy_upload_status() {
let container_element = document.getElementById(currentUser.last_ul_host);
let ul_cont_exists = document.getElementById("ul_dialog_cont_" + currentUser.last_ul_host);
if ( ul_cont_exists != undefined) {
container_element.removeChild(ul_cont_exists);
clearTimeout(currentUser["ul_timeout_" + currentUser.last_ul_host]);
}
}
async function update_upload_status(current_upload) {
console.log("Updating upload status...");
//~ console.log(current_upload);
if (current_upload.status == -1){
console.log("Destroying dialog..." + current_upload.host)
destroy_upload_status();
} else if (current_upload.status) {
console.log("Filling dialog...")
document.getElementById("ul_dialog_cont_" + current_upload.host).style.display = "block";
// Fill table
document.getElementById("ul_status_progress_cnt_" + current_upload.host).innerHTML = current_upload.progress + " / " + current_upload.total_count + t9n[LOCALE].upload_sent_count_msg;
document.getElementById("ul_status_progress_size_" + current_upload.host).innerHTML = current_upload.transferred_size + t9n[LOCALE].size_unit + t9n[LOCALE].of + current_upload.total_size + t9n[LOCALE].size_unit;
document.getElementById("ul_status_filename_" + current_upload.host).innerHTML = t9n[LOCALE].filename + ": " + current_upload.filename;
document.getElementById("ul_status_filesize_" + current_upload.host).innerHTML = t9n[LOCALE].size + ": " + current_upload.size + t9n[LOCALE].size_unit;
// Progress bar CSS
if (current_upload.transferred_percent) {
document.getElementById("ul_progress_" + current_upload.host).innerText = current_upload.transferred_percent + "%";
document.getElementById("ul_progress_" + current_upload.host).style.background = "linear-gradient(90deg, " + TIMELINE_COLOR_CURSOR + " " + current_upload.transferred_percent + "%, #fff " + current_upload.transferred_percent + "%)";
}
currentUser["ul_timeout_" + current_upload.host] = setTimeout(send_ajax_cmd, 3000, `/sync/${current_upload.host}/status`);
} else {
console.log("requesting status data");
currentUser.last_ul_host = current_upload.host;
send_ajax_cmd(`/sync/${current_upload.host}/status`);
await sleep(500);
}
}
// <div id="ul_status_10.42.0.10" class="upload_status"></div>
// Metadata display
function parse_result(command, infos_array) {
if (command == "/all/status") {
@ -419,11 +517,22 @@ function parse_result(command, infos_array) {
// Display remote media files in a table
infos_array.forEach(update_remote_filelist);
} else if (command == "/sync/status") {
// TODO : File sync UI status
currentUser.status_all = infos_array;
} else {
} else if (command.indexOf("/sync/") > -1) {
if (command.indexOf("/status") > -1 ) {
console.log("updating infos...");
console.log(infos_array);
update_upload_status(infos_array);
} else if (command.indexOf("/stop") > -1 ) {
console.log("stopping upload...");
console.log(infos_array);
destroy_upload_status();
} else {
console.log("displaying status");
//~ console.log(infos_array);
//~ if (infos_array.total_count) {
//~ display_upload_status(infos_array)
}
} else {
setTimeout(send_ajax_cmd, 80, "/all/list");
setTimeout(send_ajax_cmd, 120, "/all/status");
}
@ -437,14 +546,14 @@ function send_ajax_cmd(command) {
if (request.status === 200) {
// responseText is a string, use parse to get an array.
if (!currentUser.freeze_timeline_update) {
let infos_array = JSON.parse(request.responseText);
let infos_array = JSON.parse(request.responseText);
parse_result(command, infos_array);
}
}
}
};
request.open("GET", command, true);
request.send();
request.send();
}
@ -453,6 +562,13 @@ function enqueue_file(evt) {
setTimeout(send_ajax_cmd, 40, "/" + evt.currentTarget.host + "/list");
}
function send_btn_cmd(evt) {
let clickedButton = event.currentTarget;
let command = clickedButton.value;
send_ajax_cmd(command);
//~ setTimeout(send_ajax_cmd, 40, "/" + evt.currentTarget.host + "/list");
}
function update_statusall_content() {
document.getElementById("status_all").innerHTML = currentUser.status_all;
@ -463,7 +579,7 @@ function scan_hosts() {
send_ajax_cmd("/scan");
update_statusall_content();
setTimeout(scan_hosts, currentUser.scan_interval);
};
}
// UI
addEventListener("DOMContentLoaded", function() {
@ -502,10 +618,14 @@ addEventListener("DOMContentLoaded", function() {
} else if ( command.indexOf("/clear") > -1 || command.indexOf("/sort") > -1) {
request.onload = send_ajax_cmd(command);
} else if ( command == "/sync/all" ) {
} else if ( command.indexOf("/sync/") > -1 ) {
console.log("Sync command detected")
currentUser.status_all = t9n[LOCALE].sync;
request.onload = send_ajax_cmd("/sync/status");
// Display dialog
display_upload_status(command);
// Request values to fill dialog
//~ request.onload = send_ajax_cmd(command + "/status");
setTimeout(send_ajax_cmd, 1000, command + "/status");
}
else if ( command.indexOf("/browse") > -1 ) {
request.onload = send_ajax_cmd("/all/browse");

View File

@ -22,11 +22,61 @@ tr:nth-child(2n+1) {background-color: #888;}
border-bottom: #222 solid 1px;
display:none;
background: linear-gradient(0deg, #999 10%, #666 80%);
position:relative;
}
/*
.client_container .left_col{border-right: #a8a8a8 2px solid;}
*/
.client_container .button{}
.client_container .upload_dialog_cont {
position: absolute;
width: 100%;
background-color: #fff8;
height: 100%;
padding: 1em 0;
}
.client_container .upload_dialog {
width: 30%;
background-color: #cecece;
height: 100%;
margin: auto;
padding: 2em;
border: #00000047 2px solid;
border-radius: 5px;
color: #444;
text-align: center;
}
.client_container [id^="ul_dialog_cont_"] {
display:none;
}
.client_container .upload_dialog button {
background: #bbb;
color: #444;
width: 3em;
height: 3em;
line-height: 1em;
}
.client_container .upload_dialog .progress_bar {
background: #fff;
height: 1.4em;
border-radius: .7em;
border: #999 2px solid;
text-align: center;
transition: background 3s;
margin:.5em;
}
.client_container .upload_dialog .upload_status {}
.client_container .upload_dialog table {
margin: auto;
background: transparent;
width: 100%;
}
.client_container .upload_dialog [id^="ul_status_"] {}
/*
.timeline {height: 3em;background-color: #0f0;margin: 2em 0;}
*/
@ -133,23 +183,22 @@ tr:nth-child(2n+1) {background-color: #888;}
.col_1 {
width: 40%;
float: left;
padding: 3% 0 0 5%;
padding: 1% 0 0 2%;
}
.col_2 {
width: 59%;
display: inline-block;
clear: right;
max-height: 170px;
padding:.5em;
overflow-y: scroll;
}
.col_2 div {
overflow-y: scroll;
max-height: 170px;
}
.col_2 button {
margin:.3em !important;
background: #bbb;
color: #444;
width: 1em;
height: 1em;
line-height: 1.1em;
}
.indicator {
display:inline-block;

View File

@ -30,11 +30,13 @@
{% for host in hosts %}
<div class="client_container" id="{{ host }}">
<div class="left_col">
<h2>{{host}}</h2>
<div class="col_1">
<h2>{{host}}</h2>
<button value="/{{host}}/poweroff" class="command btn btn-block btn-lg btn-default" role="button">&#x23fb;<span class="btn_txt">{{gui_l10n['str_poweroff']}}</span></button>
<button value="/{{host}}/reboot" class="command btn btn-block btn-lg btn-default" role="button">&#x21BA;<span class="btn_txt">{{gui_l10n['str_reboot']}}</span></button>
<button value="/{{host}}/blink" class="command btn btn-block btn-lg btn-default" role="button">&#x1F4A1;<span class="btn_txt">{{gui_l10n['str_blink']}}</span></button>
<button value="/sync/{{host}}" class="command btn btn-block btn-lg btn-default" role="button">&#x1F4A1;<span class="btn_txt">{{gui_l10n['str_sync']}}</span></button>
<p id="status_{{host}}">{{status_message}}</p>
<p id="signal_{{host}}">
<span style="">{{gui_l10n['str_link']}}:</span>
@ -48,6 +50,7 @@
</div>
<div class="col_2">
<div>
<button value="/{{host}}/browse" class="command btn btn-block btn-lg btn-default" role="button">&#x21BA;<span class="btn_txt">{{gui_l10n['str_refresh']}}</span></button>
<div id="playlist_{{host}}" class="table_cont">
<table id="file_sel_{{host}}">
<tr>
@ -55,7 +58,6 @@
</tr>
</table>
</div>
<button value="/{{host}}/browse" class="command btn btn-block btn-lg btn-default" role="button">&#x21BA;<span class="btn_txt">{{gui_l10n['str_refresh']}}</span></button>
</div>
</div>
</div>