diff --git a/app.py b/app.py index eec0b61..6c71b9f 100755 --- a/app.py +++ b/app.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# pilpil-client 0.1 +# pilpil-client 0.1 # abelliqueux import base64 @@ -7,41 +7,39 @@ from flask import Flask, render_template, request, make_response, jsonify import gettext import http.client import os -# ~ import requests -# ~ import socket import ssl import subprocess 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') _ = gettext.translation('template', localedir='locales', languages=[LOCALE]).gettext -gui_l10n = { "str_pilpil_title" : _("Pilpil-server"), - "str_filename" : _("Media Files"), - "str_scan" : _("Scan"), - "str_previous" : _("Previous"), - "str_play" : _("Play"), - "str_pause" : _("Pause"), - "str_stop" : _("Stop"), - "str_next" : _("Next"), - "str_loop" : _("Loop"), - "str_repeat" : _("Repeat"), - "str_clear" : _("Clear"), - "str_sort" : _("Sort"), - "str_sync" : _("Sync"), - "str_poweroff" : _("Poweroff"), - "str_reboot" : _("Reboot"), - "str_blink" : _("Blink"), - "str_link" : _("Link"), -} +gui_l10n = {"str_pilpil_title": _("Pilpil-server"), + "str_filename": _("Media Files"), + "str_scan": _("Scan"), + "str_previous": _("Previous"), + "str_play": _("Play"), + "str_pause": _("Pause"), + "str_stop": _("Stop"), + "str_next": _("Next"), + "str_loop": _("Loop"), + "str_repeat": _("Repeat"), + "str_clear": _("Clear"), + "str_sort": _("Sort"), + "str_sync": _("Sync"), + "str_poweroff": _("Poweroff"), + "str_reboot": _("Reboot"), + "str_blink": _("Blink"), + "str_link": _("Link"), + "str_refresh": _("Refresh"), + } app = Flask(__name__) @@ -49,53 +47,53 @@ 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 - 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) ) ) + 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 = [],[] +hosts_available, hosts_unavailable = [], [] queue_msgs = [ - _("No items"), + _("No items"), _("No files queued.") - ] + ] cmd_player = { # Map vlc http url parameters to pilpil-server urls # See https://github.com/videolan/vlc/blob/1336447566c0190c42a1926464fa1ad2e59adc4f/share/lua/http/requests/README.txt - "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", - "status" : "status.xml", - "list" : "playlist.xml", - #"volume" : "volume", - #"ratio" : "aspectratio", - #"dir" : "?dir=", - #"command" : "?command=", - #"key" : "key=", - "browse" : "browse.xml?uri=file://~" - } + "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", + "status": "status.xml", + "list": "playlist.xml", + # "volume" : "volume", + # "ratio" : "aspectratio", + # "dir" : "?dir=", + # "command" : "?command=", + # "key" : "key=", + "browse": "browse.xml?uri=file://~" + } cmd_server = [ # Map pilpil-client http url parameters to pilpil-server urls "blink", "reboot", "poweroff", - "rssi", + "rssi", "sync" - ] + ] -# Configuration +# Configuration debug = app.config['DEFAULT']['debug'] pi_user = app.config['DEFAULT']['pi_user'] media_folder_remote = app.config['DEFAULT']['media_folder_remote'] @@ -110,44 +108,18 @@ cmd_port = app.config['DEFAULT']['cmd_port'] useSSL = app.config['DEFAULT']['useSSL'] CAfile = app.config['DEFAULT']['CAfile'] sync_facility = app.config['DEFAULT']['sync_facility'] -http_headers = {"Authorization":"Basic " + auth} +http_headers = {"Authorization": "Basic " + auth} # SSl context creation should be out of class 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 + sslcontext.check_hostname = False + sslcontext.verify_mode = ssl.CERT_NONE current_upload = 0 -class PilpilClient: - def __init__(self): - pass - - def __init_connection(self): - pass - - def __send_request(self): - pass - - def isup(self): - pass - def rssi(self): - pass - def status(self): - pass - def playlist(self): - pass - def command(self, cmd): - pass - - def __str__(self): - pass - - def __del__(self): - pass # Network/link utilities # https://www.metageek.com/training/resources/understanding-rssi/ @@ -156,9 +128,9 @@ def send_HTTP_request(listed_host, port, time_out=3, request_="/"): Send a http request with http auth ''' if useSSL: - conn = http.client.HTTPSConnection( listed_host + ":" + str(port), timeout=time_out, context = sslcontext ) + conn = http.client.HTTPSConnection(listed_host + ":" + str(port), timeout=time_out, context=sslcontext) else: - conn = http.client.HTTPConnection( listed_host + ":" + str(port), timeout=time_out ) + conn = http.client.HTTPConnection(listed_host + ":" + str(port), timeout=time_out) try: if debug: print(request_ + " - " + str(http_headers)) @@ -171,11 +143,12 @@ def send_HTTP_request(listed_host, port, time_out=3, request_="/"): return data except Exception as e: if debug: - print(_("Error on connection to {} : {} : {} ").format(listed_host, str(port), e)) + print(_("Error on connection to {}: {}: {} ").format(listed_host, str(port), e)) return 0 finally: conn.close() - + + def check_hosts(host_list): ''' Check hosts in a host list are up and build then return two lists with up/down hosts. @@ -188,9 +161,10 @@ def check_hosts(host_list): else: hosts_down.append(local_host) if debug: - print( _("{} of {} hosts found.").format(str(len(hosts_up)), hosts_number)) + print(_("{} of {} hosts found.").format(str(len(hosts_up)), hosts_number)) return hosts_up, hosts_down + def HTTP_upload(filename, host_local, port, trailing_slash=1): ''' Build HTTP file upload request and send it. @@ -198,7 +172,7 @@ def HTTP_upload(filename, host_local, port, trailing_slash=1): url = "https://" + host_local + ":" + str(port) + "/upload" if not trailing_slash: filename = "/" + filename - files = { "file":( filename, open( media_folder_local + filename, "rb"), "multipart/form-data") } + files = {"file": (filename, open(media_folder_local + filename, "rb"), "multipart/form-data")} if debug: print(files) resp = requests.post(url, files=files, headers=http_headers, verify=CAfile) @@ -209,6 +183,7 @@ def HTTP_upload(filename, host_local, port, trailing_slash=1): else: return 0 + 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 @@ -222,18 +197,18 @@ def sync_media_folder(media_folder_local, media_folder_remote, host_local, port, trailing_slash = 0 if media_folder_remote[-1:] != "/": media_folder_remote += "/" - #Using http_upload + # Using http_upload if sync_facility == "http": media_list = list_media_files(media_folder_local) for media in media_list: current_upload = media.strip("/") - if debug: + 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)) + 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 + transfer_ok += 1 # ~ return _("{} files uploaded.").format(str(transfer_ok)) # ~ return _("{} files uploaded.").format(str(transfer_ok)) # Using system cmd @@ -244,41 +219,45 @@ def sync_media_folder(media_folder_local, media_folder_remote, host_local, port, 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.append("--include='*." + media_type + "'") sync_args.extend(["--exclude='*'", media_folder_local, host_local + ":" + media_folder_remote]) # Using scp if sync_facility == "scp": media_list = list_media_files(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( host_local + ":" + media_folder_remote ) - sync_proc = subprocess.run( sync_args , capture_output=True) + sync_args.append(media_folder_local + media) + sync_args.append(host_local + ":" + 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] + total_size = str(sync_proc.stdout)[scrape_index:].split(" ")[0][:-1] if debug: - print("Transferred size : " + str(total_size)) + print("Transferred size: " + str(total_size)) return total_size + def get_meta_data(host, xml_data, request_="status", m3u_=0): ''' Parse XML response from pilpil-client instance and return a dict of metadata according to request type. ''' # Basic metadata media_infos = { - 'host': host, + 'host': host, 'status': 0 } if request_ == "list": # Return current instance's playlist return get_playlist(host, xml_data, m3u_) elif request_ == "status": - # Return current instance's status ( currently playing, state, time length, etc. ) + # Return current instance's status (currently playing, state, time length, etc.) if xml_data.findall("./information/category/"): for leaf in xml_data.findall("./information/category/"): if leaf.get("name") == "filename": + if debug: + print(leaf.text) filename = leaf.text + break else: filename = "N/A" cur_length = int(xml_data.find('length').text) @@ -290,10 +269,10 @@ def get_meta_data(host, xml_data, request_="status", m3u_=0): cur_loop = xml_data.find('loop').text cur_repeat = xml_data.find('repeat').text media_infos.update({ - 'status' : 1, - 'file': filename, - 'time': cur_time_fmtd, - 'leng': cur_length_fmtd, + 'status': 1, + 'file': filename, + 'time': cur_time_fmtd, + 'leng': cur_length_fmtd, 'pos': cur_pos, 'loop': cur_loop, 'repeat': cur_repeat, @@ -306,7 +285,7 @@ def get_meta_data(host, xml_data, request_="status", m3u_=0): if xml_data.findall("rssi"): media_infos.update({ 'status': 1, - 'rssi' : xml_data.find('rssi').text + 'rssi': xml_data.find('rssi').text }) elif request_ == "browse": host_medias = {} @@ -314,26 +293,24 @@ def get_meta_data(host, xml_data, request_="status", m3u_=0): media_name = media.attrib["name"] if len(media_name.split('.')) > 1: if media_name.split('.')[1] in media_exts: - host_medias[media_name] = { + host_medias[media_name] = { "name": media_name, - "uri": media.attrib["uri"], + "uri": media.attrib["uri"], "size": round(int(media.attrib["size"])/1000000, 2)} - media_infos = {host : host_medias} + 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 # VLC's playlist node name can change according to the locale on the client, so check for this too. if xml_data.find("./node") and (xml_data.find("./node").get('name') == "Playlist" or xml_data.find("./node").get('name') == _("Playlist")): - print("HAHAHAH") playlist = xml_data.findall("./node/leaf") - content_format = "{};{};{};" - if m3u: m3u_hdr = "#EXTM3U\n" m3u_prefix = "#EXTINF:" @@ -341,13 +318,13 @@ def get_playlist(host, xml_data, m3u=0): # M3U file building m3u_format = "{}{}, {}\n{}\n" m3u_content = m3u_hdr - + for item in playlist: - # item info + # item info if m3u: m3u_content += m3u_format.format(m3u_prefix, item.get("duration"), item.get("name"), item.get("uri")) item_info = content_format.format(item.get("id"), item.get("name"), sec2min(int(item.get("duration")))) - # Add cursor to currently playing element + # Add cursor to currently playing element if "current" in item.keys(): item_info += item.get("current") item_list.append(item_info) @@ -357,16 +334,17 @@ def get_playlist(host, xml_data, m3u=0): if m3u: print(m3u_content) playlist_overview = { - 'host' : host, + 'host': host, 'status': 1, - 'leng' : str(len(playlist)), - 'duration' : sec2min(playlist_duration), - 'items' : item_list - } + 'leng': str(len(playlist)), + 'duration': sec2min(playlist_duration), + 'items': item_list + } if debug: print(playlist_overview) return playlist_overview + def send_pilpil_command(host, arg0, arg1, arg2): ''' Builds a pilpil request according to args, send it and return parsed result. @@ -394,29 +372,31 @@ def send_pilpil_command(host, arg0, arg1, arg2): elif arg0 != "status": # Build request for VLC command HTTP_request = HTTP_request + "?command=" + cmd_player[arg0] - if arg1 != "null" : + if arg1 != "null": if (arg0 == "play") or (arg0 == "delete") or (arg0 == "sort") or (arg0 == "move"): # Add 'id' url parameter HTTP_request = HTTP_request + "&id=" + arg1 - if (arg0 == "sort") or (arg0 == "move") : - # Add 'val' url parameter - # val possible values : id, title, title nodes first, artist, genre, random, duration, title numeric, album + if (arg0 == "sort") or (arg0 == "move"): + # Add 'val' url parameter for "sort" + # 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 + # For "move", 'val' should be the id of the playlist item we want to move arg1 after. HTTP_request = HTTP_request + "&val=" + escape_str(arg2) - elif arg0 == "seek" : + elif arg0 == "seek": HTTP_request = HTTP_request + "&val=" + arg1 - elif (arg0 == "enqueue") or (arg0 == "add") : + elif (arg0 == "enqueue") or (arg0 == "add"): # Add 'input' url parameter HTTP_request = HTTP_request + "&input=file://" + quote(media_folder_remote_expanded) + "/" + arg1 - + # Send request and get data response data = send_HTTP_request(host, port_, time_out=3, request_=HTTP_request) if debug: - print(str(host) + " - data length :" + str(len(data))) + if data: + print(str(host) + " - data length:" + str(len(data))) if not data: print("No data was received.") return 0 - + # Parse xml data xml = ET.fromstring(data) # Process parsed data and return dict @@ -425,13 +405,14 @@ def send_pilpil_command(host, arg0, arg1, arg2): print("Metadata:" + str(metadata)) return metadata + # Utilities def list_local_media_files(folder): ''' List files in folder which extension is allowed (exists in media_exts). ''' if os.path.exists(folder): - files = os.listdir(folder); + files = os.listdir(folder) medias = [] for fd in files: if len(fd.split('.')) > 1: @@ -440,8 +421,10 @@ def list_local_media_files(folder): return medias else: return [] + + # /requests/status.xml?command=in_enqueue&input=file:///home/pi/tst1.mp4 -def write_M3U(m3u_content : str, host : str): +def write_M3U(m3u_content: str, host: str): ''' Write a M3U file named host.m3u from m3u_content ''' @@ -450,67 +433,73 @@ def write_M3U(m3u_content : str, host : str): fd.write(m3u_content) fd.close() return 1 - + + def escape_str(uri): ''' Replace spaces with %20 for http urls ''' return uri.replace(" ", "%20") + def sec2min(duration): ''' Convert seconds to min:sec format. ''' - return('%02d:%02d' % (duration / 60, duration % 60)) + return ('%02d:%02d' % (duration / 60, duration % 60)) + status_message = _("Idle") + @app.route("/") def main(): status_message = _("Searching network for live hosts...") templateData = { - 'hosts' : hosts, - 'status_message' : status_message, - 'queue_msgs' : queue_msgs, - 'gui_l10n' : gui_l10n - } + 'hosts': hosts, + 'status_message': status_message, + 'queue_msgs': queue_msgs, + 'gui_l10n': gui_l10n + } return render_template('main.html', **templateData) + @app.route("/scan") def scan(): global hosts_available, hosts_unavailable hosts_available, hosts_unavailable = check_hosts(hosts) return [hosts_available, hosts_unavailable] - + + @app.route("/browse_local") def browse(): return list_local_media_files(media_folder_local) + @app.route("/sync/") def sync(host): - # TODO : Add feedback for transfer in GUI + # TODO: Add feedback for transfer in GUI size = 0 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_expanded, hostl, cmd_port) - # ~ th = threading.Thread(target=blink_pi, args=(16,)) - # ~ th.start() - else: + else: size = sync_media_folder(media_folder_local, media_folder_remote_expanded, host, cmd_port) return str(size) - -@app.route("///", defaults = { "arg1": "null", "arg2": "null" }) -@app.route("////", defaults = { "arg2": "null" }) + + +@app.route("///", defaults={"arg1": "null", "arg2": "null"}) +@app.route("////", defaults={"arg2": "null"}) @app.route("////") def action(host, arg0, arg1, arg2): status_message = "Idle" - + if (arg0 not in cmd_player) and (arg0 not in cmd_server): - status_message = "

{}

".format(_("Wrong command")) + status_message = "E:{}".format(_("Wrong command")) return status_message - + if host == "all": # Send request to all available hosts responses = [] @@ -518,16 +507,17 @@ def action(host, arg0, arg1, arg2): 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") + status_message = "

{}

".format("Host is not reachable") else: # Send request to specified host - status_message = send_pilpil_command(host, arg0, arg1, arg2) - + status_message = [send_pilpil_command(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) \ No newline at end of file + serve(app, host='127.0.0.1', port=8080) diff --git a/changelog_todo.md b/changelog_todo.md index 97068d6..6950b2f 100644 --- a/changelog_todo.md +++ b/changelog_todo.md @@ -38,9 +38,22 @@ sha256 : ec3e17fc9b41f8c5181484e9866be2d1d92cab8403210e3d22f4f689edd4cfde * 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 : +## 0.5 : 2022-11-17-videopi.img.xz +md5 : d1dd7404ec05d16c8a4179370a214821 +sha256 : 401359a84c6d60902c05602bd52fae70f0b2ecac36d550b52d14e1e3230854a6 + + * webgui : Increase live host scan when host first found + * webgui : Btn hover/press fixed + * webgui : File selection/enqueuing + * server : Disable ssl verif for isup() + * server : Blink command + * client : rt8821cu driver is back + * client : Use safe overclocking settings for rpi1, 3, 4, better memory split + * setup : Split git repo from pilpil-server + * setup : Generate/install random http auth secret at setup + * setup : Add dry, help flags + * FR localisation for server, client and install script + * pep8ification @@ -57,31 +70,29 @@ sha256 : # DOING NEXT : - # DONE : - * webgui : Increase live host scan when host first found - * webgui : Btn hover/press fixed - * webgui : file selection/enqueuing - * server : Disable ssl verif for isup() - * server : Add blink command - * client : rt8821cu driver back - * client : Use safe overclocking settings for rpi1, 3, 4, better memory split - * setup : Split git repo from pilpil-server - * setup : Generate/install http auth secret at setup - * setup : Add dry, help flags - * FR localisation for server, client and install script - * pep8ification - -# OTHER: - * get_client_rssi.sh on server + * webgui: l10n + * webgui: Add remote file enqueuing + * webgui: Fix timeline UI + * webgui: Add video thumbnails to timeline UI + * client: Only blink when running on rpi + * client: Generate video thumbnail on start with ffmpegthumbnailer, serve SVG when file not found + * server: pep8fied (except for line length) + # TODO : - * ~ python sanitization + * webgui: remove file from timeline (drag&drop to bin?) + * webgui: file sync UI freeze/status/progress bar + * * ~ Test with several rpis - * ? Scripts hotspot linux/win/mac - * ? Config sync + * ? Scripts hotspot linux : nmcli + win : https://github.com/JamesCullum/Windows-Hotspot + mac : https://apple.stackexchange.com/questions/2488/start-stop-internet-sharing-from-a-script * ! Remove git personal details/resolv.conf, remove authorized_keys, ssh config, clean home, re-enable ssh pw login * ~ Doc + +# OTHER: + * get_client_rssi.sh on server diff --git a/locales/en/LC_MESSAGES/template.pot b/locales/en/LC_MESSAGES/template.pot index 2e57c1e..654e56b 100644 --- a/locales/en/LC_MESSAGES/template.pot +++ b/locales/en/LC_MESSAGES/template.pot @@ -83,6 +83,10 @@ msgstr "" msgid "Link" msgstr "" +#: ../app.py:43 +msgid "Refresh" +msgstr "" + #: ../app.py:52 msgid "Found configuration file in {}" msgstr "" diff --git a/locales/fr/LC_MESSAGES/template.mo b/locales/fr/LC_MESSAGES/template.mo index f1562a2..059baa5 100644 Binary files a/locales/fr/LC_MESSAGES/template.mo and b/locales/fr/LC_MESSAGES/template.mo differ diff --git a/locales/fr/LC_MESSAGES/template.pot b/locales/fr/LC_MESSAGES/template.pot index 81dd1de..faf04a9 100644 --- a/locales/fr/LC_MESSAGES/template.pot +++ b/locales/fr/LC_MESSAGES/template.pot @@ -90,6 +90,10 @@ msgstr "Clignoter" msgid "Link" msgstr "Lien" +#: ../app.py:43 +msgid "Refresh" +msgstr "Rafraîchir" + #: app.py:22 msgid "Found configuration file in {}" msgstr "Fichier de configuration trouvé dans {}" diff --git a/static/script.js b/static/script.js index 1cbdd04..ff522dd 100644 --- a/static/script.js +++ b/static/script.js @@ -1,84 +1,128 @@ -// Timeline drag and drop -const tl_cont_attr = {id:"tl_cont", ondrop: "drop(event, this)", ondragover:"allowDrop(event)"}; +// 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)"}; // Config +var DEBUG = 0; +var cmd_port = "8888"; var timeline_color_cursor = "#FF8839"; -var timeline_color_bg = "#2EB8E6"; -var scanInterval = 3000; -var status_all = "Searching network for live hosts..." +var timeline_color_bg = "#2EB8E600"; +var scan_interval = 3000; +var status_all = "Searching network for live hosts..."; // Global vars -var src_id = ""; +var src_elem = ""; var medias_status = {}; var fileButtons = []; +var freeze_timeline_update = 0; -function updatePlaylist(){ - var new_list = []; - var media_count = document.getElementById("timeline").children.length; - for (i=media_count,l=0;i>l;i--) { - toMove = document.getElementById("timeline").children[i-1].children[0].getAttribute("media_id"); - console.log(toMove); - sendCmd("/all/move/" + toMove + "/1"); - } +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); } -function findTargetIndex(element, index, array, thisArg){ +async function update_playlist(host){ + var media_count = document.getElementById("timeline_" + host).children.length; + // Reversed loop + for (i=media_count,l=0;i>l;i--) { + //~ for (i=0,l=media_count;i-1){ elem = elem.children[child]; @@ -86,13 +130,19 @@ function addAttr(id, attr, val , child=-1) { var att = document.createAttribute(attr); att.value = val; elem.setAttributeNode(att); -}; +} -function addElement(type, attr, meta = 0, j = 0){ +function add_HTML_element(type, attr, meta=0, j=0){ + // Add HTML element with type 'type' and attributes 'attr'. + // 'attr' should be a javascript object + // 'meta' should be an array + // 'j' is used to pass an increment value when used in a loop + // var elem = document.createElement(type); var keys_array = Object.keys(attr); for (i=0, l=keys_array.length;i -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 = status_all; } else if ( command.indexOf("/sort") > -1 ){ if (command.indexOf('/1/') > -1 ) { - clickedButton.value = clickedButton.value.replace('/1/','/0/') + clickedButton.value = clickedButton.value.replace('/1/','/0/'); } else { - clickedButton.value = clickedButton.value.replace('/0/','/1/') + 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 + // AJAX request var request = new XMLHttpRequest(); - if ( command == "/scan" ) { - request.onload = sendCmd(command); + if ( command == "/scan") { + request.onload = send_ajax_cmd(command); + } else if ( command.indexOf("/clear") > -1 || command.indexOf("/sort") > -1) { + request.onload = send_ajax_cmd(command); } else if ( command == "/sync/all" ) { - status_all = "Syncing files..." - request.onload = sendCmd("/sync/status"); - }; + status_all = "Syncing files..."; + request.onload = send_ajax_cmd("/sync/status"); + } - // On construit la commande + // build request request.open("GET", command, true); - // et on l'envoie + // send request request.send(); }); } }, true); -// Affichage des infos -function parseResult(command, infos_array) { - switch (command) { - case "/all/status": +// Metadata display +function parse_result(command, infos_array) { + if (command == "/all/status") { + // Requests current status of every instances, especially current media file's name, position and length. + // Also retrieves loop and repeat status. + // // Iterate over array for (var i = 0, l=infos_array.length; i " + infos_array[i].time + " / " + infos_array[i].leng; medias_status[infos_array[i].id] = infos_array[i].pos; + + // Highlight currently playing element + timeline_medias_array = Array.from(document.querySelectorAll('[media_id]')); + for (var z=0, x=timeline_medias_array.length; z" + - //~ "Id" + - //~ "Filename" + - //~ "Duration" + - //~ ""; - for (var j = 0, k=items_array.length; j" + item_meta[0] + "" + - //~ "" + item_meta[1] + "" + - //~ "" + item_meta[2] + "" + - //~ "" ; - // 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 + ")"; + } + } else if ( command.indexOf("/list") > -1 ) { + // Requests playlist of every instances + // + if (!freeze_timeline_update){ + console.log("Timeline freeze : " + freeze_timeline_update); + console.log("Infos array : " + infos_array); + for (var i = 0, l=infos_array.length; i" + "Filename" + @@ -293,8 +333,8 @@ function parseResult(command, infos_array) { } html_table += ""; document.getElementById("filelist").innerHTML = html_table; - break; - case "/all/rssi": + } else if ( command == "/all/rssi") { + // RSSI strength indicator var signal_color = 40; var best_rssi = 30; var worst_rssi = 70; @@ -304,90 +344,89 @@ function parseResult(command, infos_array) { // Reset to grey for (i=0, l=4; i4?4:rssi_norm; i" + - //~ "" + - //~ "Filename" + - //"Size" + - //~ ""; for ( var j=0, k=hosts.length;j" + item_meta["name"] + "" + - //"" + item_meta["size"] + "" + - //~ "" ; 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) + td.innerText = item_meta.name; + // Add an event to the created element to send enqueue command when a file is clicked in the list + tr.addEventListener("click", enqueue_file, false); document.getElementById("file_sel_"+hosts).appendChild(tr); - }; + } } - //~ html_table += ""; - //~ 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": + } else if ( command == "/sync/status") { status_all = infos_array; - break; - }; // End switch case -}; + } else { + setTimeout(send_ajax_cmd, 80, "/all/list"); + setTimeout(send_ajax_cmd, 120, "/all/status"); + } +} -function sendCmd(command) { +function send_ajax_cmd(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; - }; - }; + if (!freeze_timeline_update) { + var infos_array = JSON.parse(request.responseText); + //console.log(infos_array.length); + parse_result(command, infos_array); + //return infos_array; + } + } + } }; // On construit la commande request.open("GET", command, true); // et on l'envoie request.send(); -}; +} -function enqueueFile(evt) { +function enqueue_file(evt) { //console.log(evt.currentTarget.innerText + evt.currentTarget.host); - sendCmd("/" + evt.currentTarget.host + "/enqueue/" + evt.currentTarget.innerText); -}; + send_ajax_cmd("/" + evt.currentTarget.host + "/enqueue/" + evt.currentTarget.innerText); + //~ send_ajax_cmd("/all/list"); + setTimeout(send_ajax_cmd, 40, "/" + evt.currentTarget.host + "/list"); + //~ send_ajax_cmd("/" + evt.currentTarget.host + "/list"); +} -function updateStatusAll() { +function update_statusall_content() { 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); +var scan_hosts = function() { + send_ajax_cmd("/scan"); + update_statusall_content(); + setTimeout(scan_hosts, scan_interval); +} + +send_ajax_cmd("/scan"); +send_ajax_cmd("/browse_local"); +setInterval(send_ajax_cmd, 500, "/all/status"); +setTimeout(send_ajax_cmd, 1000, "/all/list"); +setTimeout(send_ajax_cmd, 3000, "/all/browse"); +setInterval(send_ajax_cmd, 20000, "/all/rssi"); +//~ setInterval(update_statusall_content, 1000); +setTimeout(scan_hosts, scan_interval); diff --git a/static/style.css b/static/style.css index 645e6d6..89d1475 100644 --- a/static/style.css +++ b/static/style.css @@ -23,14 +23,18 @@ tr:nth-child(2n+1) {background-color: #888;} display:none; background: linear-gradient(0deg, #999 10%, #666 80%); } +/* +.client_container .left_col{border-right: #a8a8a8 2px solid;} +*/ .client_container .button{} /* .timeline {height: 3em;background-color: #0f0;margin: 2em 0;} */ -#timeline { +.timeline { height: 75px; width:100%; margin-top:1em; + background-color: #33333361; } [id^="tl_cont"] { float: left; @@ -47,6 +51,8 @@ tr:nth-child(2n+1) {background-color: #888;} line-height: 75px; width:100%; background-color:#1F7B99; + background-size: 100% 100%; + box-shadow: inset 0 0 20px #0008; } [id^="tl_cont"]:nth-child(2n+1) [id^="tl_drag"] { background-color:#255E70; @@ -117,11 +123,17 @@ tr:nth-child(2n+1) {background-color: #888;} width: 59%; display: inline-block; clear: right; - height: 170px; + max-height: 170px; + padding:.5em; } .col_2 div { overflow-y: scroll; - height: 170px; + max-height: 170px; +} +.col_2 button { + margin:.3em !important; + background: #bbb; + color: #444; } .indicator { display:inline-block; @@ -129,6 +141,10 @@ tr:nth-child(2n+1) {background-color: #888;} margin: 0 0 0 5%; padding: 0.3em; } + .table_cont table { + margin:0; + width:100%; +} .file_sel { width: 100%; margin: 0; diff --git a/templates/main.html b/templates/main.html index 28862fb..468a943 100644 --- a/templates/main.html +++ b/templates/main.html @@ -61,11 +61,12 @@ +
-
+