From 0d2eee5f253a75f08c1d4f258d678f756c4a4fda Mon Sep 17 00:00:00 2001 From: ABelliqueux Date: Mon, 12 Dec 2022 16:56:33 +0100 Subject: [PATCH] webgui/server: upload dialog/status --- app.py | 103 ++++++++++++++++++++------ changelog_todo.md | 8 ++- static/script.js | 172 +++++++++++++++++++++++++++++++++++++------- static/style.css | 61 ++++++++++++++-- templates/main.html | 6 +- 5 files changed, 290 insertions(+), 60 deletions(-) diff --git a/app.py b/app.py index 128d5ef..39ebb56 100755 --- a/app.py +++ b/app.py @@ -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/") -def sync(host): - # TODO: Add feedback for transfer in GUI - size = 0 - if host == "status": +@app.route("/sync//", defaults={"arg0": "null"}) +@app.route("/sync///") +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("///", defaults={"arg1": "null", "arg2": "null"}) diff --git a/changelog_todo.md b/changelog_todo.md index 720e7a9..0c0ad74 100644 --- a/changelog_todo.md +++ b/changelog_todo.md @@ -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 diff --git a/static/script.js b/static/script.js index c8d1125..7e82ff7 100644 --- a/static/script.js +++ b/static/script.js @@ -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 : " éléments envoyé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 = ` + + + + + + + + + + + + + + +
${t9n[LOCALE].sync}
+ ` + let upload_dialog_HTML = ` +
+
+
+ +
+ ` + 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); + } +} + +//
// 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"); diff --git a/static/style.css b/static/style.css index e143cec..e3d9ba2 100644 --- a/static/style.css +++ b/static/style.css @@ -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; diff --git a/templates/main.html b/templates/main.html index 8e72e89..108ff83 100644 --- a/templates/main.html +++ b/templates/main.html @@ -30,11 +30,13 @@ {% for host in hosts %}
-

{{host}}

+
+

{{host}}

+

{{status_message}}

{{gui_l10n['str_link']}}: @@ -48,6 +50,7 @@

+
@@ -55,7 +58,6 @@
-