Add video thumbnails, pep8, timeline overhaul

This commit is contained in:
ABelliqueux 2022-11-25 19:46:41 +01:00
parent d8d8793807
commit a6d6072ac1
8 changed files with 460 additions and 395 deletions

248
app.py
View File

@ -7,13 +7,10 @@ from flask import Flask, render_template, request, make_response, jsonify
import gettext import gettext
import http.client import http.client
import os import os
# ~ import requests
# ~ import socket
import ssl import ssl
import subprocess import subprocess
from shutil import which from shutil import which
import sys import sys
# ~ import threading
import toml import toml
from urllib.parse import quote, unquote from urllib.parse import quote, unquote
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@ -24,24 +21,25 @@ from waitress import serve
LOCALE = os.getenv('LANG', 'en') LOCALE = os.getenv('LANG', 'en')
_ = gettext.translation('template', localedir='locales', languages=[LOCALE]).gettext _ = gettext.translation('template', localedir='locales', languages=[LOCALE]).gettext
gui_l10n = { "str_pilpil_title" : _("Pilpil-server"), gui_l10n = {"str_pilpil_title": _("Pilpil-server"),
"str_filename" : _("Media Files"), "str_filename": _("Media Files"),
"str_scan" : _("Scan"), "str_scan": _("Scan"),
"str_previous" : _("Previous"), "str_previous": _("Previous"),
"str_play" : _("Play"), "str_play": _("Play"),
"str_pause" : _("Pause"), "str_pause": _("Pause"),
"str_stop" : _("Stop"), "str_stop": _("Stop"),
"str_next" : _("Next"), "str_next": _("Next"),
"str_loop" : _("Loop"), "str_loop": _("Loop"),
"str_repeat" : _("Repeat"), "str_repeat": _("Repeat"),
"str_clear" : _("Clear"), "str_clear": _("Clear"),
"str_sort" : _("Sort"), "str_sort": _("Sort"),
"str_sync" : _("Sync"), "str_sync": _("Sync"),
"str_poweroff" : _("Poweroff"), "str_poweroff": _("Poweroff"),
"str_reboot" : _("Reboot"), "str_reboot": _("Reboot"),
"str_blink" : _("Blink"), "str_blink": _("Blink"),
"str_link" : _("Link"), "str_link": _("Link"),
} "str_refresh": _("Refresh"),
}
app = Flask(__name__) app = Flask(__name__)
@ -49,43 +47,43 @@ app.config.from_file("defaults.toml", load=toml.load)
config_locations = ["./", "~/.", "~/.config/"] config_locations = ["./", "~/.", "~/.config/"]
for location in config_locations: for location in config_locations:
# Optional config files, ~ is expanded to $HOME on *nix, %USERPROFILE% on windows # 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): 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) ) ) print(_("Found configuration file in {}").format(os.path.expanduser(location)))
hosts_available, hosts_unavailable = [],[] hosts_available, hosts_unavailable = [], []
queue_msgs = [ queue_msgs = [
_("No items"), _("No items"),
_("No files queued.") _("No files queued.")
] ]
cmd_player = { cmd_player = {
# Map vlc http url parameters to pilpil-server urls # Map vlc http url parameters to pilpil-server urls
# See https://github.com/videolan/vlc/blob/1336447566c0190c42a1926464fa1ad2e59adc4f/share/lua/http/requests/README.txt # See https://github.com/videolan/vlc/blob/1336447566c0190c42a1926464fa1ad2e59adc4f/share/lua/http/requests/README.txt
"play" : "pl_play", "play": "pl_play",
"resume" : "pl_forceresume", "resume": "pl_forceresume",
"pause" : "pl_forcepause", "pause": "pl_forcepause",
"tpause" : "pl_pause", "tpause": "pl_pause",
"previous" : "pl_previous", "previous": "pl_previous",
"next" : "pl_next", "next": "pl_next",
"stop" : "pl_stop", "stop": "pl_stop",
"enqueue" : "in_enqueue", "enqueue": "in_enqueue",
"add" : "in_play", "add": "in_play",
"clear" : "pl_empty", "clear": "pl_empty",
"delete" : "pl_delete", "delete": "pl_delete",
"loop" : "pl_loop", "loop": "pl_loop",
"repeat" : "pl_repeat", "repeat": "pl_repeat",
"random" : "pl_random", "random": "pl_random",
"move" : "pl_move", "move": "pl_move",
"sort" : "pl_sort", "sort": "pl_sort",
"seek" : "seek", "seek": "seek",
"status" : "status.xml", "status": "status.xml",
"list" : "playlist.xml", "list": "playlist.xml",
#"volume" : "volume", # "volume" : "volume",
#"ratio" : "aspectratio", # "ratio" : "aspectratio",
#"dir" : "?dir=<uri>", # "dir" : "?dir=<uri>",
#"command" : "?command=<cmd>", # "command" : "?command=<cmd>",
#"key" : "key=", # "key" : "key=",
"browse" : "browse.xml?uri=file://~" "browse": "browse.xml?uri=file://~"
} }
cmd_server = [ cmd_server = [
# Map pilpil-client http url parameters to pilpil-server urls # Map pilpil-client http url parameters to pilpil-server urls
"blink", "blink",
@ -93,7 +91,7 @@ cmd_server = [
"poweroff", "poweroff",
"rssi", "rssi",
"sync" "sync"
] ]
# Configuration # Configuration
debug = app.config['DEFAULT']['debug'] debug = app.config['DEFAULT']['debug']
@ -110,44 +108,18 @@ cmd_port = app.config['DEFAULT']['cmd_port']
useSSL = app.config['DEFAULT']['useSSL'] useSSL = app.config['DEFAULT']['useSSL']
CAfile = app.config['DEFAULT']['CAfile'] CAfile = app.config['DEFAULT']['CAfile']
sync_facility = app.config['DEFAULT']['sync_facility'] 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 # SSl context creation should be out of class
sslcontext = ssl.create_default_context() sslcontext = ssl.create_default_context()
if os.path.exists(CAfile): if os.path.exists(CAfile):
sslcontext.load_verify_locations(cafile=CAfile) sslcontext.load_verify_locations(cafile=CAfile)
else: else:
sslcontext.check_hostname = False sslcontext.check_hostname = False
sslcontext.verify_mode = ssl.CERT_NONE sslcontext.verify_mode = ssl.CERT_NONE
current_upload = 0 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 # Network/link utilities
# https://www.metageek.com/training/resources/understanding-rssi/ # 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 Send a http request with http auth
''' '''
if useSSL: 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: else:
conn = http.client.HTTPConnection( listed_host + ":" + str(port), timeout=time_out ) conn = http.client.HTTPConnection(listed_host + ":" + str(port), timeout=time_out)
try: try:
if debug: if debug:
print(request_ + " - " + str(http_headers)) print(request_ + " - " + str(http_headers))
@ -171,11 +143,12 @@ def send_HTTP_request(listed_host, port, time_out=3, request_="/"):
return data return data
except Exception as e: except Exception as e:
if debug: 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 return 0
finally: finally:
conn.close() conn.close()
def check_hosts(host_list): def check_hosts(host_list):
''' '''
Check hosts in a host list are up and build then return two lists with up/down hosts. 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: else:
hosts_down.append(local_host) hosts_down.append(local_host)
if debug: 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 return hosts_up, hosts_down
def HTTP_upload(filename, host_local, port, trailing_slash=1): def HTTP_upload(filename, host_local, port, trailing_slash=1):
''' '''
Build HTTP file upload request and send it. 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" url = "https://" + host_local + ":" + str(port) + "/upload"
if not trailing_slash: if not trailing_slash:
filename = "/" + filename 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: if debug:
print(files) print(files)
resp = requests.post(url, files=files, headers=http_headers, verify=CAfile) 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: else:
return 0 return 0
def sync_media_folder(media_folder_local, media_folder_remote, host_local, port, sync_facility=sync_facility): 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 Sync media_folder_local with media_folder_remote using sync_facility
@ -222,7 +197,7 @@ def sync_media_folder(media_folder_local, media_folder_remote, host_local, port,
trailing_slash = 0 trailing_slash = 0
if media_folder_remote[-1:] != "/": if media_folder_remote[-1:] != "/":
media_folder_remote += "/" media_folder_remote += "/"
#Using http_upload # Using http_upload
if sync_facility == "http": if sync_facility == "http":
media_list = list_media_files(media_folder_local) media_list = list_media_files(media_folder_local)
for media in media_list: for media in media_list:
@ -231,7 +206,7 @@ def sync_media_folder(media_folder_local, media_folder_remote, host_local, port,
print("Uploading " + str(media)) print("Uploading " + str(media))
if HTTP_upload(media, host_local, port, trailing_slash): if HTTP_upload(media, host_local, port, trailing_slash):
if debug: if debug:
print("File size : " + str(os.stat(media_folder_local + media).st_size)) print("File size: " + str(os.stat(media_folder_local + media).st_size))
total_size += 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))
@ -244,23 +219,24 @@ def sync_media_folder(media_folder_local, media_folder_remote, host_local, port,
scrape_str = "total size is " scrape_str = "total size is "
sync_args = [sync_facility, "-zharm", "--include='*/'"] sync_args = [sync_facility, "-zharm", "--include='*/'"]
for media_type in media_exts: 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]) sync_args.extend(["--exclude='*'", media_folder_local, host_local + ":" + media_folder_remote])
# Using scp # Using scp
if sync_facility == "scp": if sync_facility == "scp":
media_list = list_media_files(media_folder_local) media_list = list_media_files(media_folder_local)
sync_args = [sync_facility, "-Crp", "-o IdentitiesOnly=yes"] sync_args = [sync_facility, "-Crp", "-o IdentitiesOnly=yes"]
for media in media_list: for media in media_list:
sync_args.append( media_folder_local + media ) sync_args.append(media_folder_local + media)
sync_args.append( host_local + ":" + media_folder_remote ) sync_args.append(host_local + ":" + media_folder_remote)
sync_proc = subprocess.run( sync_args , capture_output=True) sync_proc = subprocess.run(sync_args, capture_output=True)
if len(sync_proc.stdout): if len(sync_proc.stdout):
scrape_index = str(sync_proc.stdout).index(scrape_str)+len(scrape_str) 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: if debug:
print("Transferred size : " + str(total_size)) print("Transferred size: " + str(total_size))
return total_size return total_size
def get_meta_data(host, xml_data, request_="status", m3u_=0): 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. Parse XML response from pilpil-client instance and return a dict of metadata according to request type.
@ -274,11 +250,14 @@ def get_meta_data(host, xml_data, request_="status", m3u_=0):
# Return current instance's playlist # Return current instance's playlist
return get_playlist(host, xml_data, m3u_) return get_playlist(host, xml_data, m3u_)
elif request_ == "status": 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/"): if xml_data.findall("./information/category/"):
for leaf in xml_data.findall("./information/category/"): for leaf in xml_data.findall("./information/category/"):
if leaf.get("name") == "filename": if leaf.get("name") == "filename":
if debug:
print(leaf.text)
filename = leaf.text filename = leaf.text
break
else: else:
filename = "N/A" filename = "N/A"
cur_length = int(xml_data.find('length').text) cur_length = int(xml_data.find('length').text)
@ -290,7 +269,7 @@ def get_meta_data(host, xml_data, request_="status", m3u_=0):
cur_loop = xml_data.find('loop').text cur_loop = xml_data.find('loop').text
cur_repeat = xml_data.find('repeat').text cur_repeat = xml_data.find('repeat').text
media_infos.update({ media_infos.update({
'status' : 1, 'status': 1,
'file': filename, 'file': filename,
'time': cur_time_fmtd, 'time': cur_time_fmtd,
'leng': cur_length_fmtd, 'leng': cur_length_fmtd,
@ -306,7 +285,7 @@ def get_meta_data(host, xml_data, request_="status", m3u_=0):
if xml_data.findall("rssi"): if xml_data.findall("rssi"):
media_infos.update({ media_infos.update({
'status': 1, 'status': 1,
'rssi' : xml_data.find('rssi').text 'rssi': xml_data.find('rssi').text
}) })
elif request_ == "browse": elif request_ == "browse":
host_medias = {} host_medias = {}
@ -318,22 +297,20 @@ def get_meta_data(host, xml_data, request_="status", m3u_=0):
"name": media_name, "name": media_name,
"uri": media.attrib["uri"], "uri": media.attrib["uri"],
"size": round(int(media.attrib["size"])/1000000, 2)} "size": round(int(media.attrib["size"])/1000000, 2)}
media_infos = {host : host_medias} media_infos = {host: host_medias}
if debug: if debug:
print(media_infos) print(media_infos)
return media_infos return media_infos
def get_playlist(host, xml_data, m3u=0): def get_playlist(host, xml_data, m3u=0):
playlist = [] playlist = []
item_list = [] item_list = []
playlist_duration = 0 playlist_duration = 0
# VLC's playlist node name can change according to the locale on the client, so check for this too. # 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")): 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") playlist = xml_data.findall("./node/leaf")
content_format = "{};{};{};" content_format = "{};{};{};"
if m3u: if m3u:
m3u_hdr = "#EXTM3U\n" m3u_hdr = "#EXTM3U\n"
m3u_prefix = "#EXTINF:" m3u_prefix = "#EXTINF:"
@ -357,16 +334,17 @@ def get_playlist(host, xml_data, m3u=0):
if m3u: if m3u:
print(m3u_content) print(m3u_content)
playlist_overview = { playlist_overview = {
'host' : host, 'host': host,
'status': 1, 'status': 1,
'leng' : str(len(playlist)), 'leng': str(len(playlist)),
'duration' : sec2min(playlist_duration), 'duration': sec2min(playlist_duration),
'items' : item_list 'items': item_list
} }
if debug: if debug:
print(playlist_overview) print(playlist_overview)
return playlist_overview return playlist_overview
def send_pilpil_command(host, arg0, arg1, arg2): def send_pilpil_command(host, arg0, arg1, arg2):
''' '''
Builds a pilpil request according to args, send it and return parsed result. Builds a pilpil request according to args, send it and return parsed result.
@ -394,25 +372,27 @@ def send_pilpil_command(host, arg0, arg1, arg2):
elif arg0 != "status": elif arg0 != "status":
# Build request for VLC command # Build request for VLC command
HTTP_request = HTTP_request + "?command=" + cmd_player[arg0] 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"): if (arg0 == "play") or (arg0 == "delete") or (arg0 == "sort") or (arg0 == "move"):
# Add 'id' url parameter # Add 'id' url parameter
HTTP_request = HTTP_request + "&id=" + arg1 HTTP_request = HTTP_request + "&id=" + arg1
if (arg0 == "sort") or (arg0 == "move") : if (arg0 == "sort") or (arg0 == "move"):
# Add 'val' url parameter # Add 'val' url parameter for "sort"
# val possible values : id, title, title nodes first, artist, genre, random, duration, title numeric, album # 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 # 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) HTTP_request = HTTP_request + "&val=" + escape_str(arg2)
elif arg0 == "seek" : elif arg0 == "seek":
HTTP_request = HTTP_request + "&val=" + arg1 HTTP_request = HTTP_request + "&val=" + arg1
elif (arg0 == "enqueue") or (arg0 == "add") : elif (arg0 == "enqueue") or (arg0 == "add"):
# Add 'input' url parameter # Add 'input' url parameter
HTTP_request = HTTP_request + "&input=file://" + quote(media_folder_remote_expanded) + "/" + arg1 HTTP_request = HTTP_request + "&input=file://" + quote(media_folder_remote_expanded) + "/" + arg1
# Send request and get data response # Send request and get data response
data = send_HTTP_request(host, port_, time_out=3, request_=HTTP_request) data = send_HTTP_request(host, port_, time_out=3, request_=HTTP_request)
if debug: if debug:
print(str(host) + " - data length :" + str(len(data))) if data:
print(str(host) + " - data length:" + str(len(data)))
if not data: if not data:
print("No data was received.") print("No data was received.")
return 0 return 0
@ -425,13 +405,14 @@ def send_pilpil_command(host, arg0, arg1, arg2):
print("Metadata:" + str(metadata)) print("Metadata:" + str(metadata))
return metadata return metadata
# Utilities # Utilities
def list_local_media_files(folder): def list_local_media_files(folder):
''' '''
List files in folder which extension is allowed (exists in media_exts). List files in folder which extension is allowed (exists in media_exts).
''' '''
if os.path.exists(folder): if os.path.exists(folder):
files = os.listdir(folder); files = os.listdir(folder)
medias = [] medias = []
for fd in files: for fd in files:
if len(fd.split('.')) > 1: if len(fd.split('.')) > 1:
@ -440,8 +421,10 @@ def list_local_media_files(folder):
return medias return medias
else: else:
return [] return []
# /requests/status.xml?command=in_enqueue&input=file:///home/pi/tst1.mp4 # /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 Write a M3U file named host.m3u from m3u_content
''' '''
@ -451,64 +434,70 @@ def write_M3U(m3u_content : str, host : str):
fd.close() fd.close()
return 1 return 1
def escape_str(uri): def escape_str(uri):
''' '''
Replace spaces with %20 for http urls Replace spaces with %20 for http urls
''' '''
return uri.replace(" ", "%20") return uri.replace(" ", "%20")
def sec2min(duration): def sec2min(duration):
''' '''
Convert seconds to min:sec format. Convert seconds to min:sec format.
''' '''
return('%02d:%02d' % (duration / 60, duration % 60)) return ('%02d:%02d' % (duration / 60, duration % 60))
status_message = _("Idle") status_message = _("Idle")
@app.route("/") @app.route("/")
def main(): def main():
status_message = _("Searching network for live hosts...") status_message = _("Searching network for live hosts...")
templateData = { templateData = {
'hosts' : hosts, 'hosts': hosts,
'status_message' : status_message, 'status_message': status_message,
'queue_msgs' : queue_msgs, 'queue_msgs': queue_msgs,
'gui_l10n' : gui_l10n 'gui_l10n': gui_l10n
} }
return render_template('main.html', **templateData) return render_template('main.html', **templateData)
@app.route("/scan") @app.route("/scan")
def scan(): def scan():
global hosts_available, hosts_unavailable global hosts_available, hosts_unavailable
hosts_available, hosts_unavailable = check_hosts(hosts) hosts_available, hosts_unavailable = check_hosts(hosts)
return [hosts_available, hosts_unavailable] return [hosts_available, hosts_unavailable]
@app.route("/browse_local") @app.route("/browse_local")
def browse(): def browse():
return list_local_media_files(media_folder_local) return list_local_media_files(media_folder_local)
@app.route("/sync/<host>") @app.route("/sync/<host>")
def sync(host): def sync(host):
# TODO : Add feedback for transfer in GUI # TODO: Add feedback for transfer in GUI
size = 0 size = 0
if host == "status": if host == "status":
return str(current_upload) return str(current_upload)
elif host == "all": elif host == "all":
for hostl in hosts_available: for hostl in hosts_available:
size += sync_media_folder(media_folder_local, media_folder_remote_expanded, hostl, cmd_port) 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) size = sync_media_folder(media_folder_local, media_folder_remote_expanded, host, cmd_port)
return str(size) return str(size)
@app.route("/<host>/<arg0>/", defaults = { "arg1": "null", "arg2": "null" })
@app.route("/<host>/<arg0>/<arg1>/", defaults = { "arg2": "null" }) @app.route("/<host>/<arg0>/", defaults={"arg1": "null", "arg2": "null"})
@app.route("/<host>/<arg0>/<arg1>/", defaults={"arg2": "null"})
@app.route("/<host>/<arg0>/<arg1>/<arg2>") @app.route("/<host>/<arg0>/<arg1>/<arg2>")
def action(host, arg0, arg1, arg2): def action(host, arg0, arg1, arg2):
status_message = "Idle" status_message = "Idle"
if (arg0 not in cmd_player) and (arg0 not in cmd_server): if (arg0 not in cmd_player) and (arg0 not in cmd_server):
status_message = "<p>{}</p>".format(_("Wrong command")) status_message = "E:{}".format(_("Wrong command"))
return status_message return status_message
if host == "all": if host == "all":
@ -518,16 +507,17 @@ def action(host, arg0, arg1, arg2):
responses.append(send_pilpil_command(hostl, arg0, arg1, arg2)) responses.append(send_pilpil_command(hostl, arg0, arg1, arg2))
status_message = responses status_message = responses
elif host not in hosts_available: elif host not in hosts_available:
status_message = "<p>{}</p>".format("Host is not reachable") status_message = "<p>{}</p>".format("Host is not reachable")
else: else:
# Send request to specified host # 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: if debug:
print(status_message) print(status_message)
return status_message return status_message
if __name__ == '__main__': if __name__ == '__main__':
# ~ app.run() # ~ app.run()
serve(app, host='127.0.0.1', port=8080) serve(app, host='127.0.0.1', port=8080)

View File

@ -38,9 +38,22 @@ sha256 : ec3e17fc9b41f8c5181484e9866be2d1d92cab8403210e3d22f4f689edd4cfde
* Add media folder sync (scp, rsync, http upload) * 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 * 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 ## 0.5 : 2022-11-17-videopi.img.xz
md5 : md5 : d1dd7404ec05d16c8a4179370a214821
sha256 : 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
@ -58,30 +71,28 @@ sha256 :
# DOING NEXT : # DOING NEXT :
# DONE : # 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
* 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)
# OTHER:
* get_client_rssi.sh on server
# TODO : # TODO :
* ~ python sanitization * webgui: remove file from timeline (drag&drop to bin?)
* webgui: file sync UI freeze/status/progress bar
*
* ~ Test with several rpis * ~ Test with several rpis
* ? Scripts hotspot linux/win/mac * ? Scripts hotspot linux : nmcli
* ? Config sync 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 * ! Remove git personal details/resolv.conf, remove authorized_keys, ssh config, clean home, re-enable ssh pw login
* ~ Doc * ~ Doc
# OTHER:
* get_client_rssi.sh on server

View File

@ -83,6 +83,10 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "" msgstr ""
#: ../app.py:43
msgid "Refresh"
msgstr ""
#: ../app.py:52 #: ../app.py:52
msgid "Found configuration file in {}" msgid "Found configuration file in {}"
msgstr "" msgstr ""

Binary file not shown.

View File

@ -90,6 +90,10 @@ msgstr "Clignoter"
msgid "Link" msgid "Link"
msgstr "Lien" msgstr "Lien"
#: ../app.py:43
msgid "Refresh"
msgstr "Rafraîchir"
#: app.py:22 #: app.py:22
msgid "Found configuration file in {}" msgid "Found configuration file in {}"
msgstr "Fichier de configuration trouvé dans {}" msgstr "Fichier de configuration trouvé dans {}"

View File

@ -1,84 +1,128 @@
// Timeline drag and drop // Timeline drag and drop elements default attributes
const tl_cont_attr = {id:"tl_cont", ondrop: "drop(event, this)", ondragover:"allowDrop(event)"}; 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_drag_attr = {id:"tl_drag", draggable:"true", ondragstart:"drag(event, this)"};
// Config // Config
var DEBUG = 0;
var cmd_port = "8888";
var timeline_color_cursor = "#FF8839"; var timeline_color_cursor = "#FF8839";
var timeline_color_bg = "#2EB8E6"; var timeline_color_bg = "#2EB8E600";
var scanInterval = 3000; var scan_interval = 3000;
var status_all = "Searching network for live hosts..." var status_all = "Searching network for live hosts...";
// Global vars // Global vars
var src_id = ""; var src_elem = "";
var medias_status = {}; var medias_status = {};
var fileButtons = []; var fileButtons = [];
var freeze_timeline_update = 0;
function updatePlaylist(){ function sleep(ms) {
var new_list = []; return new Promise(resolve => setTimeout(resolve, ms));
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 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<l;i++) {
// Find current's timeline element children's 'media_id' value
toShift = document.getElementById("timeline_" + host).children[i-1].children[0].getAttribute("media_id");
console.log(toShift + " : " + document.getElementById("timeline_" + host).children[i-1].children[0].innerText);
// Move 'toShift' after element with id '1'
// In VLC's playlist XML representation, playlist gets id '1', so moving to that id
// really means moving to the the very start of the playlist.
send_ajax_cmd("/" + host + "/move/" + toShift + "/1");
await sleep(80);
}
// Un-freeze timeline update flag
freeze_timeline_update = 0;
}
function find_target_index(element, index, array, thisArg){
if (element == this) { if (element == this) {
return index+1; return index + 1;
} }
return 0; return 0;
}; }
function moveElements(target) { function shift_elements(target_elem) {
var elem_list = Array.from(src_id.parentElement.children); // Shift elements in the timeline UI
//
// Get a list of current element siblings
var elem_list = Array.from(src_elem.parentElement.children);
// Initiate slice
var elem_list_slice = elem_list; var elem_list_slice = elem_list;
var source_idx = elem_list.findIndex(findTargetIndex, src_id); // Find indexes of source and target elements in the timeline
var target_idx = elem_list.findIndex(findTargetIndex, target); var source_idx = elem_list.findIndex(find_target_index, src_elem);
var target_idx = elem_list.findIndex(find_target_index, target_elem);
var idx; var idx;
if (source_idx < target_idx) { // idx is the index where the elements should be inserted back
elem_list_slice = elem_list.slice(source_idx+1, target_idx+1); // Target element is on the left of the source element
idx = source_idx; if (source_idx < target_idx){
elem_list_slice = elem_list.slice(source_idx + 1, target_idx + 1);
idx = source_idx;
} else { } else {
elem_list_slice = elem_list.slice(target_idx, source_idx ); // Target element is on the right of the source element
idx = target_idx+1; elem_list_slice = elem_list.slice(target_idx, source_idx );
idx = target_idx + 1;
} }
for (i=0, l=elem_list_slice.length; i<l;i++) { // Shift elements according to idx
elem_list[idx+i].appendChild(elem_list_slice[i].children[0]); for (i=0, l=elem_list_slice.length; i<l;i++){
elem_list[idx + i].appendChild(elem_list_slice[i].children[0]);
} }
return target; return target_elem;
}; }
function allowDrop(ev) { function allow_drop(ev) {
ev.preventDefault(); ev.preventDefault();
drop_id = ev.dataTransfer.getData("text"); drop_id = ev.dataTransfer.getData("text");
}; }
function drag(ev, source) { function drag(ev, source) {
src_id = ev.target.parentElement; // Freeze timeline update flag
freeze_timeline_update = 1;
src_elem = ev.target.parentElement;
if (DEBUG) {
console.log(src_elem.children[0].getAttribute("media_id") + " : " + src_elem.children[0].innerText);
}
ev.dataTransfer.setData("text", ev.target.id); ev.dataTransfer.setData("text", ev.target.id);
}; }
function drop(ev, target) { function drop(ev, target_elem) {
// If the currently dragged element is dropped on another element, shift HTML elements in the timeline to the left or right from the target element.
//
ev.preventDefault(); ev.preventDefault();
// Get dragged element id
var data = ev.dataTransfer.getData("text"); var data = ev.dataTransfer.getData("text");
if (src_id.id != target.id) { // Only shift if not dropping on self
dropTarget = moveElements(target); if (src_elem.id != target_elem.id) {
dropTarget = shift_elements(target_elem);
if (dropTarget) { if (dropTarget) {
//
dropTarget.appendChild(document.getElementById(data)); dropTarget.appendChild(document.getElementById(data));
updatePlaylist(); // Update VLC playlist according to UI timeline
// TODO : This currently sends the move cmd to all instances. Fix it to specify which host to send the cmd to.
update_playlist("10.42.0.10");
} }
} }
}; send_ajax_cmd("/all/list");
//~ setTimeout(function(){ freeze_timeline_update = 0; console.log("shwing!"); }, 2000);
function adjustTl() { }
var child = document.getElementById('timeline').children;
function adjust_timeline() {
// Adapt timeline's UI elements to fit the width of their parent container.
//
//~ var child = document.getElementById('timeline').children;
var child = document.querySelector('[id^="timeline_"]').children;
var divWidth = 100 / child.length; var divWidth = 100 / child.length;
for (i=0, l=child.length;i<l;i++) { for (i=0, l=child.length;i<l;i++) {
child[i].style.width= divWidth + "%"; child[i].style.width= divWidth + "%";
} }
}; }
function addAttr(id, attr, val , child=-1) { function add_HTML_attr(id, attr, val , child=-1) {
// Add attribute 'attr' with value 'val' to the HTML element with id 'id'.
// If child is other than -1, the attribute is applied to the HTML element's child at position 'child'
var elem = document.getElementById(id); var elem = document.getElementById(id);
if (child>-1){ if (child>-1){
elem = elem.children[child]; elem = elem.children[child];
@ -86,13 +130,19 @@ function addAttr(id, attr, val , child=-1) {
var att = document.createAttribute(attr); var att = document.createAttribute(attr);
att.value = val; att.value = val;
elem.setAttributeNode(att); 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 elem = document.createElement(type);
var keys_array = Object.keys(attr); var keys_array = Object.keys(attr);
for (i=0, l=keys_array.length;i<l;i++) { for (i=0, l=keys_array.length;i<l;i++) {
var att = document.createAttribute(keys_array[i]); var att = document.createAttribute(keys_array[i]);
// First iteration
if(!i){ if(!i){
att.value = Object.values(attr)[i]+j; att.value = Object.values(attr)[i]+j;
} else { } else {
@ -100,45 +150,37 @@ function addElement(type, attr, meta = 0, j = 0){
} }
elem.setAttributeNode(att); elem.setAttributeNode(att);
} }
// Set playlist id attribute // Set media_id attribute if metadata is provided
if (meta) { if (meta) {
att = document.createAttribute("media_id"); attribute = document.createAttribute("media_id");
att.value = meta[0]; attribute.value = meta[0];
elem.setAttributeNode(att); elem.setAttributeNode(attribute);
} }
// Get filename // Get filename
elem.innerText = meta[1]; elem.innerText = meta[1];
return elem; return elem;
}; }
// Bouttons de commande // Bouttons de commande
addEventListener("DOMContentLoaded", function() { addEventListener("DOMContentLoaded", function() {
sendCmd("/scan"); //~ send_ajax_cmd("/scan");
sendCmd("/all/list"); //~ send_ajax_cmd("/browse_local");
sendCmd("/browse_local"); //~ setTimeout(send_ajax_cmd, 3000, "/all/browse");
setTimeout(sendCmd, 3000, "/all/browse"); //~ setTimeout(send_ajax_cmd, 4000, "/all/rssi");
setTimeout(sendCmd, 4000, "/all/rssi"); //~ setTimeout(send_ajax_cmd, 1000, "/all/list");
adjustTl(); adjust_timeline();
// Get filename when selected in table // Get all elements with class 'command'
//~ for (var i=0, l=fileButtons.length; i<l; i++) {
//~ var selected_file = fileButtons[i];
//~ // Sur un click
//~ selected_file.addEventListener("click", function(event) {
//~ // On intercepte le signal
//~ event.preventDefault();
//~ }
//~ }
// Tous les elements avec la classe ".command"
var commandButtons = document.querySelectorAll(".command"); var commandButtons = document.querySelectorAll(".command");
for (var i=0, l=commandButtons.length; i<l; i++) { for (var i=0, l=commandButtons.length; i<l; i++) {
var button = commandButtons[i]; var button = commandButtons[i];
// Sur un click // Listen for clicks
button.addEventListener("click", function(event) { button.addEventListener("click", function(event) {
// On intercepte le signal // Discard signal
event.preventDefault(); event.preventDefault();
// On recupere la valeur de value="" sur le bouton // Get value=""
var clickedButton = event.currentTarget; var clickedButton = event.currentTarget;
var command = clickedButton.value; var command = clickedButton.value;
// Proceed accordingly
if ( command.indexOf("/reboot" ) > -1 || command.indexOf("/poweroff") > -1 ) { if ( command.indexOf("/reboot" ) > -1 || command.indexOf("/poweroff") > -1 ) {
if ( !confirm("Êtes vous certain de vouloir effectuer cette action ?") ) { if ( !confirm("Êtes vous certain de vouloir effectuer cette action ?") ) {
return 0; return 0;
@ -149,120 +191,118 @@ addEventListener("DOMContentLoaded", function() {
} else if ( command.indexOf("/sort") > -1 ){ } else if ( command.indexOf("/sort") > -1 ){
if (command.indexOf('/1/') > -1 ) { if (command.indexOf('/1/') > -1 ) {
clickedButton.value = clickedButton.value.replace('/1/','/0/') clickedButton.value = clickedButton.value.replace('/1/','/0/');
} else { } 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(); var request = new XMLHttpRequest();
if ( command == "/scan" ) { if ( command == "/scan") {
request.onload = sendCmd(command); 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" ) { } else if ( command == "/sync/all" ) {
status_all = "Syncing files..." status_all = "Syncing files...";
request.onload = sendCmd("/sync/status"); request.onload = send_ajax_cmd("/sync/status");
}; }
// On construit la commande // build request
request.open("GET", command, true); request.open("GET", command, true);
// et on l'envoie // send request
request.send(); request.send();
}); });
} }
}, true); }, true);
// Affichage des infos // Metadata display
function parseResult(command, infos_array) { function parse_result(command, infos_array) {
switch (command) { if (command == "/all/status") {
case "/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 // Iterate over array
for (var i = 0, l=infos_array.length; i<l; i++) { for (var i = 0, l=infos_array.length; i<l; i++) {
// Get filename, time/length // Get filename, time/length
if (infos_array[i].status) { if (infos_array[i].status) {
document.getElementById("status_"+infos_array[i].host).innerHTML = infos_array[i].file + " <br/> " + infos_array[i].time + " / " + infos_array[i].leng; document.getElementById("status_"+infos_array[i].host).innerHTML = infos_array[i].file + " <br/> " + infos_array[i].time + " / " + infos_array[i].leng;
medias_status[infos_array[i].id] = infos_array[i].pos; 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<x;z++){
if ( timeline_medias_array[z].getAttribute("media_id") == infos_array[i].id ) {
var pos = infos_array[i].pos * 100;
var pos1 = pos-1 + "%";
pos = pos + "%";
var media_url_str = "url(https://" + infos_array[i].host + ":" + cmd_port + "/thumb/" + infos_array[i].file + ")"
var media_cssgrad_rule = "linear-gradient(90deg," + timeline_color_bg + " " + pos1 + ", " + timeline_color_cursor + " " + pos + ", " + timeline_color_bg + " " + pos + ")," + media_url_str;
timeline_medias_array[z].style.backgroundImage = media_cssgrad_rule;
timeline_medias_array[z].style.borderBottom = "4px solid " + timeline_color_cursor;
} else {
var media_url_str = "url(https://" + infos_array[i].host + ":" + cmd_port + "/thumb/" + timeline_medias_array[z].innerText + ")"
timeline_medias_array[z].style.backgroundImage = media_url_str;
timeline_medias_array[z].style.borderBottom = "None";
}
}
} else { } else {
document.getElementById("status_"+infos_array[i].host).innerHTML = "<br><br>"; document.getElementById("status_"+infos_array[i].host).innerHTML = "<br><br>";
} }
// Toggle loop indicator
if (infos_array[i].loop == "true") { if (infos_array[i].loop == "true") {
document.getElementById("loop_ind_" + infos_array[i].host).style.backgroundColor = "#78E738" document.getElementById("loop_ind_" + infos_array[i].host).style.backgroundColor = "#78E738";
} else { } else {
document.getElementById("loop_ind_" + infos_array[i].host).style.backgroundColor = "#A42000" document.getElementById("loop_ind_" + infos_array[i].host).style.backgroundColor = "#A42000";
}; }
// Toggle repeat indicator // Toggle repeat indicator
if (infos_array[i].repeat == "true") { if (infos_array[i].repeat == "true") {
document.getElementById("repeat_ind_" + infos_array[i].host).style.backgroundColor = "#78E738" document.getElementById("repeat_ind_" + infos_array[i].host).style.backgroundColor = "#78E738";
} else { } else {
document.getElementById("repeat_ind_" + infos_array[i].host).style.backgroundColor = "#A42000" document.getElementById("repeat_ind_" + infos_array[i].host).style.backgroundColor = "#A42000";
};
};
break;
case "/all/list":
for (var i = 0, l=infos_array.length; i<l; i++) {
// Fill Playlist infos
//~ document.getElementById("playlist_"+infos_array[i].host).innerHTML = infos_array[i].leng + " item(s) in playlist - " + infos_array[i].duration;
// Build html table and timeline
var items_array = Array.from(infos_array[i].items);
//console.log(items_array.length);
if (items_array.length == 0){
var child_list = Array.from(document.getElementById("timeline").children);
for(i=0,l=child_list.length;i<l;i++){
document.getElementById("timeline").removeChild(child_list[i]);
};
break;
} }
//~ var html_table = "<table>" + }
//~ "<tr>" + } else if ( command.indexOf("/list") > -1 ) {
//~ "<th>Id</th>" + // Requests playlist of every instances
//~ "<th>Filename</th>" + //
//~ "<th>Duration</th>" + if (!freeze_timeline_update){
//~ "</tr>"; console.log("Timeline freeze : " + freeze_timeline_update);
for (var j = 0, k=items_array.length; j<k; j++) { console.log("Infos array : " + infos_array);
// Table for (var i = 0, l=infos_array.length; i<l; i++) {
item_meta = items_array[j].split(';'); // Playlist infos are displayed in a div ; number of items in list and total duration.
//~ html_table += "<tr>" + //~ document.getElementById("playlist_"+infos_array[i].host).innerHTML = infos_array[i].leng + " item(s) in playlist - " + infos_array[i].duration;
//~ "<td>" + item_meta[0] + "</td>" + // Populate timeline according to the content of the playlist
//~ "<td>" + item_meta[1] + "</td>" + // Get returned items as an array
//~ "<td>" + item_meta[2] + "</td>" + var items_array = Array.from(infos_array[i].items);
//~ "</tr>" ; //console.log(items_array.length);
// Timeline // If playlist is empty, remove all media divs in the timeline UI.
var child_node = addElement("div", tl_drag_attr, item_meta, j); if (items_array.length == 0){
var len = document.getElementById("timeline").children.length; var child_list = Array.from(document.getElementById("timeline_" + infos_array[i].host).children);
addAttr("timeline", "length", len); for(x=0,y=child_list.length;x<y;x++){
if ( len < items_array.length ) { document.getElementById("timeline_" + infos_array[i].host).removeChild(child_list[x]);
document.getElementById("timeline").appendChild( addElement("div", tl_cont_attr, 0, len) ); }
break;
} }
document.getElementById(tl_cont_attr.id + j).replaceChildren(child_node); for (var j = 0, k=items_array.length; j<k; j++) {
// Adjust elements width // Timeline
adjustTl(); item_meta = items_array[j].split(';');
// Highlight currently playing element var child_node = add_HTML_element("div", tl_drag_attr, item_meta, j);
if (item_meta[3] != ""){ var len = document.getElementById("timeline_" + infos_array[i].host).children.length;
document.getElementById(tl_cont_attr.id + j).children[0].style.borderBottom = "4px solid " + timeline_color_cursor; add_HTML_attr("timeline_" + infos_array[i].host, "length", len);
document.getElementById(tl_cont_attr.id + j).children[0].style.fontWeight = "bold"; if ( len < items_array.length ) {
var pos = medias_status[item_meta[0]] * 100; document.getElementById("timeline_" + infos_array[i].host).appendChild( add_HTML_element("div", tl_cont_attr, 0, len) );
//~ pos = pos.toPrecision(2); }
var pos1 = pos-1 + "%"; document.getElementById(tl_cont_attr.id + j).replaceChildren(child_node);
pos = pos + "%"; var media_name = document.getElementById(tl_cont_attr.id + j).children[0].innerText;
//console.log( "linear-gradient(90deg," + timeline_color2 + " " + pos1 + ", " + timeline_color1 + " " + pos + ", " + timeline_color2 + " " + pos + ")" ); var media_url_str = "url(https://" + infos_array[i].host + ":" + cmd_port + "/thumb/" + media_name + ")"
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 + ")"; document.getElementById(tl_cont_attr.id + j).children[0].style.backgroundImage = media_url_str;
// Adjust elements width
adjust_timeline();
} }
} }
//~ html_table += "</table>"; }
//~ document.getElementById("playlist_"+infos_array[i].host).innerHTML += html_table; } else if ( command == "/scan") {
};
break;
case "/scan":
var host_up = infos_array[0]; var host_up = infos_array[0];
var host_down = infos_array[1]; var host_down = infos_array[1];
for ( var i=0, l=host_up.length; i<l; i++){ for ( var i=0, l=host_up.length; i<l; i++){
@ -272,14 +312,14 @@ function parseResult(command, infos_array) {
document.getElementById(host_down[i]).style.display = 'none'; document.getElementById(host_down[i]).style.display = 'none';
} }
if (host_up.length) { if (host_up.length) {
scanInterval = 10000; scan_interval = 10000;
//~ document.getElementById("status_all").innerHTML = "Scan interval set to " + scanInterval; //~ document.getElementById("status_all").innerHTML = "Scan interval set to " + scan_interval;
status_all = "Scan interval set to " + scanInterval; status_all = "Scan interval set to " + scan_interval;
} }
//~ 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)."; status_all = host_up.length + " client(s).";
break; } else if ( command == "/browse_local") {
case "/browse_local": // Display local media files in a table
var html_table = "<table>" + var html_table = "<table>" +
"<tr>" + "<tr>" +
"<th>Filename</th>" + "<th>Filename</th>" +
@ -293,8 +333,8 @@ function parseResult(command, infos_array) {
} }
html_table += "</table>"; html_table += "</table>";
document.getElementById("filelist").innerHTML = html_table; document.getElementById("filelist").innerHTML = html_table;
break; } else if ( command == "/all/rssi") {
case "/all/rssi": // RSSI strength indicator
var signal_color = 40; var signal_color = 40;
var best_rssi = 30; var best_rssi = 30;
var worst_rssi = 70; var worst_rssi = 70;
@ -304,90 +344,89 @@ function parseResult(command, infos_array) {
// Reset to grey // Reset to grey
for (i=0, l=4; i<l;i++) { for (i=0, l=4; i<l;i++) {
document.getElementById("wl_"+i).style.backgroundColor = "hsl(0, 0%, 65%)"; document.getElementById("wl_"+i).style.backgroundColor = "hsl(0, 0%, 65%)";
}; }
// Color it // Color it
for (i=0, l=rssi_norm>4?4:rssi_norm; i<l;i++) { for (i=0, l=rssi_norm>4?4:rssi_norm; i<l;i++) {
document.getElementById("wl_"+i).style.backgroundColor = "hsl(" + signal_color + ", 100%, 50%)"; document.getElementById("wl_"+i).style.backgroundColor = "hsl(" + signal_color + ", 100%, 50%)";
}; }
}; }
break; } else if ( command == "/all/browse") {
case "/all/browse": // Display remote media files in a table
for (var i=0, l=infos_array.length; i<l; i++) { for (var i=0, l=infos_array.length; i<l; i++) {
hosts = Object.keys(infos_array[i]); hosts = Object.keys(infos_array[i]);
//~ console.log(keys); //~ console.log(keys);
//~ var html_table = "<table id='file_sel_" + hosts + "'>" +
//~ "<tr>" +
//~ "<th>Filename</th>" +
//"<th>Size</th>" +
//~ "</tr>";
for ( var j=0, k=hosts.length;j<k;j++ ) { for ( var j=0, k=hosts.length;j<k;j++ ) {
keys_ = Object.keys(infos_array[i][hosts[j]]); keys_ = Object.keys(infos_array[i][hosts[j]]);
//~ console.log(infos_array[i][hosts[j]]); //~ console.log(infos_array[i][hosts[j]]);
for ( var m=0, n=keys_.length;m<n;m++ ) { for ( var m=0, n=keys_.length;m<n;m++ ) {
item_meta = infos_array[i][hosts[j]][keys_[m]]; item_meta = infos_array[i][hosts[j]][keys_[m]];
//~ console.log(item_meta); //~ console.log(item_meta);
//~ html_table += "<tr>" +
//~ "<td class='.file_selection'>" + item_meta["name"] + "</td>" +
//"<td>" + item_meta["size"] + "</td>" +
//~ "</tr>" ;
tr = document.createElement("tr"); tr = document.createElement("tr");
td = document.createElement("td"); td = document.createElement("td");
tr.appendChild(td); tr.appendChild(td);
tr.setAttribute("class", "file_selection"); tr.setAttribute("class", "file_selection");
tr.host = hosts[j]; tr.host = hosts[j];
td.innerText = item_meta["name"]; td.innerText = item_meta.name;
tr.addEventListener("click", enqueueFile, false) // 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); document.getElementById("file_sel_"+hosts).appendChild(tr);
}; }
} }
//~ html_table += "</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; } else if ( command == "/sync/status") {
case "/sync/status":
status_all = infos_array; status_all = infos_array;
break; } else {
}; // End switch case 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(); var request = new XMLHttpRequest();
request.onload = function() { request.onload = function() {
if (request.readyState === request.DONE) { if (request.readyState === request.DONE) {
if (request.status === 200) { if (request.status === 200) {
// responseText is a string, use parse to get an array. // responseText is a string, use parse to get an array.
var infos_array = JSON.parse(request.responseText); if (!freeze_timeline_update) {
//console.log(infos_array); var infos_array = JSON.parse(request.responseText);
parseResult(command, infos_array); //console.log(infos_array.length);
//return infos_array; parse_result(command, infos_array);
}; //return infos_array;
}; }
}
}
}; };
// On construit la commande // On construit la commande
request.open("GET", command, true); request.open("GET", command, true);
// et on l'envoie // et on l'envoie
request.send(); request.send();
}; }
function enqueueFile(evt) { function enqueue_file(evt) {
//console.log(evt.currentTarget.innerText + evt.currentTarget.host); //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; document.getElementById("status_all").innerHTML = status_all;
}; }
setInterval(sendCmd, 500, "/all/status"); var scan_hosts = function() {
setInterval(sendCmd, 1000, "/all/list"); send_ajax_cmd("/scan");
//~ setInterval(sendCmd, 3000, "/all/browse"); update_statusall_content();
setInterval(sendCmd, scanInterval, "/scan"); setTimeout(scan_hosts, scan_interval);
setInterval(sendCmd, 20000, "/all/rssi"); }
setInterval(updateStatusAll, 1000);
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);

View File

@ -23,14 +23,18 @@ tr:nth-child(2n+1) {background-color: #888;}
display:none; display:none;
background: linear-gradient(0deg, #999 10%, #666 80%); background: linear-gradient(0deg, #999 10%, #666 80%);
} }
/*
.client_container .left_col{border-right: #a8a8a8 2px solid;}
*/
.client_container .button{} .client_container .button{}
/* /*
.timeline {height: 3em;background-color: #0f0;margin: 2em 0;} .timeline {height: 3em;background-color: #0f0;margin: 2em 0;}
*/ */
#timeline { .timeline {
height: 75px; height: 75px;
width:100%; width:100%;
margin-top:1em; margin-top:1em;
background-color: #33333361;
} }
[id^="tl_cont"] { [id^="tl_cont"] {
float: left; float: left;
@ -47,6 +51,8 @@ tr:nth-child(2n+1) {background-color: #888;}
line-height: 75px; line-height: 75px;
width:100%; width:100%;
background-color:#1F7B99; background-color:#1F7B99;
background-size: 100% 100%;
box-shadow: inset 0 0 20px #0008;
} }
[id^="tl_cont"]:nth-child(2n+1) [id^="tl_drag"] { [id^="tl_cont"]:nth-child(2n+1) [id^="tl_drag"] {
background-color:#255E70; background-color:#255E70;
@ -117,11 +123,17 @@ tr:nth-child(2n+1) {background-color: #888;}
width: 59%; width: 59%;
display: inline-block; display: inline-block;
clear: right; clear: right;
height: 170px; max-height: 170px;
padding:.5em;
} }
.col_2 div { .col_2 div {
overflow-y: scroll; overflow-y: scroll;
height: 170px; max-height: 170px;
}
.col_2 button {
margin:.3em !important;
background: #bbb;
color: #444;
} }
.indicator { .indicator {
display:inline-block; display:inline-block;
@ -129,6 +141,10 @@ tr:nth-child(2n+1) {background-color: #888;}
margin: 0 0 0 5%; margin: 0 0 0 5%;
padding: 0.3em; padding: 0.3em;
} }
.table_cont table {
margin:0;
width:100%;
}
.file_sel { .file_sel {
width: 100%; width: 100%;
margin: 0; margin: 0;

View File

@ -61,11 +61,12 @@
</tr> </tr>
</table> </table>
</div> </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> </div>
</div> </div>
<div class="right_col"> <div class="right_col">
<div id="timeline"> <div class="timeline" id="timeline_{{host}}">
<!-- <!--
<div id="tl_contX"></div> <div id="tl_contX"></div>
--> -->