From 82a67286eaabf0b94312d41ecc679da49516cc48 Mon Sep 17 00:00:00 2001 From: ABelliqueux Date: Sat, 12 Nov 2022 18:17:25 +0100 Subject: [PATCH] Media enqueuing UI --- app.py | 103 +++++++++----- changelog_todo.md | 5 +- locales/fr/LC_MESSAGES/template.mo | Bin 1244 -> 1302 bytes locales/fr/LC_MESSAGES/template.pot | 4 + pilpil-server.toml | 7 +- static/script.js | 203 ++++++++++++++++++---------- static/style.css | 50 ++++--- templates/main.html | 10 +- 8 files changed, 253 insertions(+), 129 deletions(-) diff --git a/app.py b/app.py index 85733ad..650ee0e 100755 --- a/app.py +++ b/app.py @@ -7,15 +7,18 @@ from flask import Flask, render_template, request, make_response, jsonify import gettext import http.client import os -import socket +# ~ import requests +# ~ import socket import ssl import subprocess -import requests from shutil import which import sys +# ~ import threading import toml +from urllib.parse import quote, unquote import xml.etree.ElementTree as ET from waitress import serve + # l10n LOCALE = os.getenv('LANG', 'en') @@ -30,8 +33,6 @@ for location in config_locations: if app.config.from_file(os.path.expanduser( location + "pilpil-server.toml"), load=toml.load, silent=True): print( _("Found configuration file in {}").format( os.path.expanduser(location) ) ) -### - hosts_available, hosts_unavailable = [],[] queue_msgs = [ _("No items"), @@ -57,7 +58,6 @@ cmd_player = { "move" : "pl_move", "sort" : "pl_sort", "seek" : "seek", - "sync" : "sync", "status" : "status.xml", "list" : "playlist.xml", #"volume" : "volume", @@ -65,19 +65,20 @@ cmd_player = { #"dir" : "?dir=", #"command" : "?command=", #"key" : "key=", - #"browse" : "browse.xml?uri=file://~" + "browse" : "browse.xml?uri=file://~" } cmd_server = [ # Map pilpil-client http url parameters to pilpil-server urls "blink", "reboot", "poweroff", - "rssi" + "rssi", + "sync" ] # Configuration debug = app.config['DEFAULT']['debug'] -media_folder_remote = app.config['DEFAULT']['media_folder_remote'] +media_folder_remote = os.path.expanduser(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']['vlc_auth']).encode('utf-8')), 'utf-8') @@ -98,6 +99,7 @@ else: sslcontext.check_hostname = False sslcontext.verify_mode = ssl.CERT_NONE +current_upload = 0 class PilpilClient: def __init__(self): @@ -190,6 +192,8 @@ def sync_media_folder(media_folder_local, media_folder_remote, host_local, port, ''' Sync media_folder_local with media_folder_remote using sync_facility ''' + total_size = 0 + transfer_ok = 0 trailing_slash = 1 # Check for trailing / and add it if missing if media_folder_local[-1:] != "/": @@ -200,10 +204,18 @@ def sync_media_folder(media_folder_local, media_folder_remote, host_local, port, #Using http_upload if sync_facility == "http": media_list = list_media_files(media_folder_local) - transfer_ok = 0 for media in media_list: - transfer_ok += HTTP_upload(media, host_local, port, trailing_slash) - return _("{} files uploaded.").format(str(transfer_ok)) + current_upload = media.strip("/") + if debug: + print("Uploading " + str(media)) + if HTTP_upload(media, host_local, port, trailing_slash): + if debug: + print("File size : " + str(os.stat(media_folder_local + media).st_size)) + total_size += os.stat(media_folder_local + media).st_size + transfer_ok += 1 + # ~ return _("{} files uploaded.").format(str(transfer_ok)) + # ~ return _("{} files uploaded.").format(str(transfer_ok)) + # Using system cmd elif which(sync_facility): # Build subprocess arg list accroding to sync_facility # Using Rsync @@ -224,9 +236,9 @@ def sync_media_folder(media_folder_local, media_folder_remote, host_local, port, 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 + if debug: + print("Transferred size : " + str(total_size)) + return total_size def get_meta_data(host, xml_data, request_="status", m3u_=0): ''' @@ -239,7 +251,7 @@ def get_meta_data(host, xml_data, request_="status", m3u_=0): } if request_ == "list": # Return current instance's playlist - return get_playlist(host, xml_data, m3u_) + return get_playlist(host, xml_data, m3u_) elif request_ == "status": # Return current instance's status ( currently playing, state, time length, etc. ) if xml_data.findall("./information/category/"): @@ -267,22 +279,37 @@ def get_meta_data(host, xml_data, request_="status", m3u_=0): 'id': cur_id, }) elif request_ == "rssi": - # # Return current instance's wifi signal quality - print(xml_data) + # Return current instance's wifi signal quality + if debug: + print(xml_data) if xml_data.findall("rssi"): media_infos.update({ 'status': 1, 'rssi' : xml_data.find('rssi').text }) - + elif request_ == "browse": + host_medias = {} + for media in xml_data: + media_name = media.attrib["name"] + if len(media_name.split('.')) > 1: + if media_name.split('.')[1] in media_exts: + host_medias[media_name] = { + "name": media_name, + "uri": media.attrib["uri"], + "size": round(int(media.attrib["size"])/1000000, 2)} + media_infos = {host : host_medias} + if debug: + print(media_infos) + return media_infos def get_playlist(host, xml_data, m3u=0): playlist = [] item_list = [] playlist_duration = 0 - - if xml_data.find("./node") and xml_data.find("./node").get('name') == "Playlist": + + if xml_data.find("./node") and xml_data.find("./node").get('name') == _("Playlist"): + print("HAHAHAH") playlist = xml_data.findall("./node/leaf") content_format = "{};{};{};" @@ -329,14 +356,22 @@ def send_pilpil_command(host, arg0, arg1, arg2): # # Default request HTTP_request = "/requests/status.xml" - if arg0 == "list" : + if arg0 == "list": # Get playlist HTTP_request = "/requests/playlist.xml" + elif arg0 == "browse": + if debug: + print("Brosing {}".format(media_folder_remote)) + # Browse remote media folder + media_folder_remote_URL = quote(media_folder_remote) + HTTP_request = "/requests/browse.xml?uri=file://{}".format(media_folder_remote_URL) + if debug: + print("requesting url {} to {}:{}".format(HTTP_request, host, port_)) elif arg0 in cmd_server: # Switching to cmd server HTTP_request = "/" + str(arg0) port_ = cmd_port - elif arg0 != "status" : + elif arg0 != "status": # Build request for VLC command HTTP_request = HTTP_request + "?command=" + cmd_player[arg0] if arg1 != "null" : @@ -352,7 +387,7 @@ def send_pilpil_command(host, arg0, arg1, arg2): HTTP_request = HTTP_request + "&val=" + arg1 elif (arg0 == "enqueue") or (arg0 == "add") : # Add 'input' url parameter - HTTP_request = HTTP_request + "&input=file://" + media_folder_remote + "/" + arg1 + HTTP_request = HTTP_request + "&input=file://" + quote(media_folder_remote) + "/" + arg1 # Send request and get data response data = send_HTTP_request(host, port_, time_out=3, request_=HTTP_request) @@ -371,7 +406,7 @@ def send_pilpil_command(host, arg0, arg1, arg2): return metadata # Utilities -def list_media_files(folder): +def list_local_media_files(folder): ''' List files in folder which extension is allowed (exists in media_exts). ''' @@ -426,20 +461,24 @@ def scan(): hosts_available, hosts_unavailable = check_hosts(hosts) return [hosts_available, hosts_unavailable] -@app.route("/browse") +@app.route("/browse_local") def browse(): - return list_media_files(media_folder_local) - + 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 == "all": + if host == "status": + return str(current_upload) + elif host == "all": for hostl in hosts_available: size += sync_media_folder(media_folder_local, media_folder_remote, hostl, cmd_port) + # ~ th = threading.Thread(target=blink_pi, args=(16,)) + # ~ th.start() else: size = sync_media_folder(media_folder_local, media_folder_remote, host, cmd_port) - return size; + return str(size) @app.route("///", defaults = { "arg1": "null", "arg2": "null" }) @app.route("////", defaults = { "arg2": "null" }) @@ -453,10 +492,10 @@ def action(host, arg0, arg1, arg2): if host == "all": # Send request to all available hosts - resp = [] + responses = [] for hostl in hosts_available: - resp.append(send_pilpil_command(hostl, arg0, arg1, arg2)) - status_message = resp + responses.append(send_pilpil_command(hostl, arg0, arg1, arg2)) + status_message = responses elif host not in hosts_available: status_message = "

{}

".format("Host is not reachable") else: diff --git a/changelog_todo.md b/changelog_todo.md index 50c16aa..e9a8605 100644 --- a/changelog_todo.md +++ b/changelog_todo.md @@ -32,7 +32,7 @@ sha256 : 0fe3fe76d0e56e445124fa20646fa8b3d8c59568786b3ebc8a96d83d92f203e3 md5 : dee7af70135994169cab4f073ee51905 sha256 : ec3e17fc9b41f8c5181484e9866be2d1d92cab8403210e3d22f4f689edd4cfde - * Switch to rpi os Bullseye + * Switch to rpi os Bullseye arm64 * 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) @@ -70,6 +70,7 @@ sha256 : * setup : Generate/install http auth secret at setup * setup : Add dry, help flags * FR localisation for server, client and install script + * pep8ification # OTHER: @@ -77,7 +78,7 @@ sha256 : # TODO : - * ? python sanitization + * ~ python sanitization * ~ Test with several rpis * ? Scripts hotspot linux/win/mac * ? Config sync diff --git a/locales/fr/LC_MESSAGES/template.mo b/locales/fr/LC_MESSAGES/template.mo index 3655f6ab629f5bfcfc465d642ac2d9f2defa85c7..ca9e8b2f63fdf010e4a8658e50ffc76262a6599c 100644 GIT binary patch delta 426 zcmXxgy-vbV6u|LQtRRT;5!6VGUL%PNyaNLv!Nkplg~33LO=_E99gG?Vm|Ph6cmfw@ zcP1A%9)P%MTpS$S{hu07a_;Z+wzucB%j7iGd&}DsAu{A1IVN|=w8cVPVi8>|;Q$YC zhWq%6Hhy9Pzc7nGxQ6kVNDec2gon6+b+t#A>cq0A RRF3I5#^c$Wt}|-b<6i}>Eq(w1 delta 345 zcmXZXI}gEN6vpw>s{1AG32Bg!5Rv!2ca^=CVNsKKcY1l;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"); @@ -21,10 +23,10 @@ function updatePlaylist(){ } function findTargetIndex(element, index, array, thisArg){ - if (element == this) { - return index+1; - } - return 0; + if (element == this) { + return index+1; + } + return 0; }; function moveElements(target) { @@ -47,26 +49,25 @@ function moveElements(target) { }; function allowDrop(ev) { - ev.preventDefault(); - drop_id = ev.dataTransfer.getData("text"); + ev.preventDefault(); + drop_id = ev.dataTransfer.getData("text"); }; function drag(ev, source) { - src_id = ev.target.parentElement; - ev.dataTransfer.setData("text", ev.target.id); + 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) { + 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() { @@ -88,36 +89,45 @@ function addAttr(id, attr, val , child=-1) { }; 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 -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--){ @@ -155,7 +168,11 @@ addEventListener("DOMContentLoaded", function() { var request = new XMLHttpRequest(); if ( command == "/scan" ) { request.onload = sendCmd(command); - } + } else if ( command == "/sync/all" ) { + status_all = "Syncing files..." + request.onload = sendCmd("/sync/status"); + }; + // On construit la commande request.open("GET", command, true); // et on l'envoie @@ -175,15 +192,6 @@ function parseResult(command, infos_array) { document.getElementById("status_"+infos_array[i].host).innerHTML = infos_array[i].file + "
" + 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" + - "Id" + - "Filename" + - "Duration" + - ""; + //~ var html_table = "" + + //~ "" + + //~ "" + + //~ "" + + //~ "" + + //~ ""; for (var j = 0, k=items_array.length; j" + item_meta[0] + "" + - "" + - "" + - "" ; + //~ html_table += "" + + //~ "" + + //~ "" + + //~ "" + + //~ "" ; // Timeline var child_node = addElement("div", tl_drag_attr, item_meta, j); var len = document.getElementById("timeline").children.length; @@ -248,8 +256,8 @@ function parseResult(command, infos_array) { 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 += "
IdFilenameDuration
" + item_meta[1] + "" + item_meta[2] + "
" + item_meta[0] + "" + item_meta[1] + "" + item_meta[2] + "
"; - document.getElementById("playlist_"+infos_array[i].host).innerHTML += html_table; + //~ html_table += ""; + //~ document.getElementById("playlist_"+infos_array[i].host).innerHTML += html_table; }; break; case "/scan": @@ -263,11 +271,13 @@ function parseResult(command, infos_array) { } if (host_up.length) { scanInterval = 10000; - document.getElementById("status_all").innerHTML = "Scan intarvel set to " + scanInterval; + //~ document.getElementById("status_all").innerHTML = "Scan interval set to " + scanInterval; + status_all = "Scan interval set to " + scanInterval; } - document.getElementById("status_all").innerHTML = host_up.length + " client(s)."; + //~ document.getElementById("status_all").innerHTML = host_up.length + " client(s)."; + status_all = host_up.length + " client(s)."; break; - case "/browse": + case "/browse_local": var html_table = "" + "" + "" + @@ -299,6 +309,47 @@ function parseResult(command, infos_array) { }; }; break; + case "/all/browse": + for (var i=0, l=infos_array.length; i" + + //~ "" + + //~ "" + + //"" + + //~ ""; + for ( var j=0, k=hosts.length;j" + item_meta["name"] + "" + + //"" + + //~ "" ; + tr = document.createElement("tr"); + td = document.createElement("td"); + tr.appendChild(td); + tr.setAttribute("class", "file_selection"); + tr.host = hosts[j]; + td.innerText = item_meta["name"]; + tr.addEventListener("click", enqueueFile, false) + document.getElementById("file_sel_"+hosts).appendChild(tr); + }; + } + //~ html_table += "
Filename
FilenameSize
" + item_meta["size"] + "
"; + //~ document.getElementById("playlist_"+hosts).innerHTML += html_table; + //~ fileButtons = document.querySelectorAll('.file_selection'); + //~ console.log(fileButtons); + //~ for (var i=fileButtons.length; i>0; i--) { + //~ fileButtons[i].addEventListener('click', selectFile, false); + //~ } + } + break; + case "/sync/status": + status_all = infos_array; + break; }; // End switch case }; @@ -321,8 +372,20 @@ function sendCmd(command) { request.send(); }; -setInterval( sendCmd, 500, "/all/status"); -setInterval( sendCmd, 1000, "/all/list"); -setInterval( sendCmd, scanInterval, "/scan"); -setInterval( sendCmd, 20000, "/all/rssi"); +function enqueueFile(evt) { + //console.log(evt.currentTarget.innerText + evt.currentTarget.host); + sendCmd("/" + evt.currentTarget.host + "/enqueue/" + evt.currentTarget.innerText); +}; + +function updateStatusAll() { + document.getElementById("status_all").innerHTML = status_all; +}; + +setInterval(sendCmd, 500, "/all/status"); +setInterval(sendCmd, 1000, "/all/list"); +//~ setInterval(sendCmd, 3000, "/all/browse"); +setInterval(sendCmd, scanInterval, "/scan"); +setInterval(sendCmd, 20000, "/all/rssi"); +setInterval(updateStatusAll, 1000); + diff --git a/static/style.css b/static/style.css index 802bc54..3ac6f70 100644 --- a/static/style.css +++ b/static/style.css @@ -1,7 +1,7 @@ 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} +table {background-color: #555;margin-left:.5em;max-width:100%;font-size:.9em} td {padding: 0;} th {background-color: #aaa;} tr:nth-child(2n+1) {background-color: #888;} @@ -87,9 +87,9 @@ tr:nth-child(2n+1) {background-color: #888;} } .btn_txt {display: block;font-size: small;} /*Right column*/ -.right_col {width: 79.9%;display: inline-block;} +.right_col {width: 71%;display: inline-block;} /*Left column*/ -.left_col {width: 20%;display: inline-block;float: left;clear: left;} +.left_col {width: 28%;display: inline-block;float: left;clear: left;} .left_col button { width: 2em; height: 2em; @@ -109,30 +109,38 @@ tr:nth-child(2n+1) {background-color: #888;} } .left_col button:hover .btn_txt {display:block;} .col_1 { - width: 40%; - float: left; - padding: 3% 0 0 5%; -} + width: 40%; + float: left; + padding: 3% 0 0 5%; } .col_2 { - width: 60%; - display: inline-block; - clear: right; + width: 59%; + display: inline-block; + clear: right; + height: 170px; +} +.col_2 div { + overflow-y: scroll; + height: 170px; } .indicator { - display:inline-block; - background-color: #C32600; - margin: 0 0 0 5%; - padding: 0.3em; + display:inline-block; + background-color: #C32600; + margin: 0 0 0 5%; + padding: 0.3em; } + .file_sel { + width: 100%; + margin: 0; +} .wl_indicator { - display: inline-block; - background-color: #bbb; - vertical-align: bottom; - margin: 0 1px; - padding: 0; - height: .5em; - width: 5%; + 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;} diff --git a/templates/main.html b/templates/main.html index 42a2479..401e284 100644 --- a/templates/main.html +++ b/templates/main.html @@ -56,7 +56,15 @@

Repeat

-

{{queue_msgs[1]}}

+
{{queue_msgs[1]}} +
+ + + + +
Filename
+
+