Add video thumbnails, pep8, timeline overhaul
This commit is contained in:
parent
d8d8793807
commit
a6d6072ac1
232
app.py
232
app.py
|
@ -7,13 +7,10 @@ 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
|
||||
|
@ -24,24 +21,25 @@ from waitress import serve
|
|||
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,10 +47,10 @@ 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 files queued.")
|
||||
|
@ -60,31 +58,31 @@ queue_msgs = [
|
|||
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=<uri>",
|
||||
#"command" : "?command=<cmd>",
|
||||
#"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=<uri>",
|
||||
# "command" : "?command=<cmd>",
|
||||
# "key" : "key=",
|
||||
"browse": "browse.xml?uri=file://~"
|
||||
}
|
||||
cmd_server = [
|
||||
# Map pilpil-client http url parameters to pilpil-server urls
|
||||
|
@ -110,7 +108,7 @@ 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()
|
||||
|
@ -122,32 +120,6 @@ else:
|
|||
|
||||
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,7 +197,7 @@ 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:
|
||||
|
@ -231,7 +206,7 @@ def sync_media_folder(media_folder_local, media_folder_remote, host_local, port,
|
|||
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))
|
||||
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))
|
||||
|
@ -244,23 +219,24 @@ 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.
|
||||
|
@ -274,11 +250,14 @@ def get_meta_data(host, xml_data, request_="status", m3u_=0):
|
|||
# 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,7 +269,7 @@ 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,
|
||||
'status': 1,
|
||||
'file': filename,
|
||||
'time': cur_time_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"):
|
||||
media_infos.update({
|
||||
'status': 1,
|
||||
'rssi' : xml_data.find('rssi').text
|
||||
'rssi': xml_data.find('rssi').text
|
||||
})
|
||||
elif request_ == "browse":
|
||||
host_medias = {}
|
||||
|
@ -318,22 +297,20 @@ def get_meta_data(host, xml_data, request_="status", m3u_=0):
|
|||
"name": media_name,
|
||||
"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:"
|
||||
|
@ -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,25 +372,27 @@ 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
|
||||
|
@ -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
|
||||
'''
|
||||
|
@ -451,64 +434,70 @@ def write_M3U(m3u_content : str, host : str):
|
|||
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/<host>")
|
||||
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:
|
||||
size = sync_media_folder(media_folder_local, media_folder_remote_expanded, host, cmd_port)
|
||||
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>")
|
||||
def action(host, arg0, arg1, arg2):
|
||||
status_message = "Idle"
|
||||
|
||||
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
|
||||
|
||||
if host == "all":
|
||||
|
@ -521,13 +510,14 @@ def action(host, arg0, arg1, arg2):
|
|||
status_message = "<p>{}</p>".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)
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
@ -58,30 +71,28 @@ 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
|
||||
|
||||
* 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 :
|
||||
|
||||
* ~ 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
|
||||
|
|
|
@ -83,6 +83,10 @@ msgstr ""
|
|||
msgid "Link"
|
||||
msgstr ""
|
||||
|
||||
#: ../app.py:43
|
||||
msgid "Refresh"
|
||||
msgstr ""
|
||||
|
||||
#: ../app.py:52
|
||||
msgid "Found configuration file in {}"
|
||||
msgstr ""
|
||||
|
|
Binary file not shown.
|
@ -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 {}"
|
||||
|
|
423
static/script.js
423
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<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) {
|
||||
return index+1;
|
||||
return index + 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
function moveElements(target) {
|
||||
var elem_list = Array.from(src_id.parentElement.children);
|
||||
function shift_elements(target_elem) {
|
||||
// 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 source_idx = elem_list.findIndex(findTargetIndex, src_id);
|
||||
var target_idx = elem_list.findIndex(findTargetIndex, target);
|
||||
// Find indexes of source and target elements in the timeline
|
||||
var source_idx = elem_list.findIndex(find_target_index, src_elem);
|
||||
var target_idx = elem_list.findIndex(find_target_index, target_elem);
|
||||
var idx;
|
||||
if (source_idx < target_idx) {
|
||||
elem_list_slice = elem_list.slice(source_idx+1, target_idx+1);
|
||||
// idx is the index where the elements should be inserted back
|
||||
// Target element is on the left of the source element
|
||||
if (source_idx < target_idx){
|
||||
elem_list_slice = elem_list.slice(source_idx + 1, target_idx + 1);
|
||||
idx = source_idx;
|
||||
} else {
|
||||
// Target element is on the right of the source element
|
||||
elem_list_slice = elem_list.slice(target_idx, source_idx );
|
||||
idx = target_idx+1;
|
||||
idx = target_idx + 1;
|
||||
}
|
||||
for (i=0, l=elem_list_slice.length; i<l;i++) {
|
||||
elem_list[idx+i].appendChild(elem_list_slice[i].children[0]);
|
||||
// Shift elements according to idx
|
||||
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();
|
||||
drop_id = ev.dataTransfer.getData("text");
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
// Get dragged element id
|
||||
var data = ev.dataTransfer.getData("text");
|
||||
if (src_id.id != target.id) {
|
||||
dropTarget = moveElements(target);
|
||||
// Only shift if not dropping on self
|
||||
if (src_elem.id != target_elem.id) {
|
||||
dropTarget = shift_elements(target_elem);
|
||||
if (dropTarget) {
|
||||
//
|
||||
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;
|
||||
for (i=0, l=child.length;i<l;i++) {
|
||||
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);
|
||||
if (child>-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<l;i++) {
|
||||
var att = document.createAttribute(keys_array[i]);
|
||||
// First iteration
|
||||
if(!i){
|
||||
att.value = Object.values(attr)[i]+j;
|
||||
} else {
|
||||
|
@ -100,45 +150,37 @@ function addElement(type, attr, meta = 0, j = 0){
|
|||
}
|
||||
elem.setAttributeNode(att);
|
||||
}
|
||||
// Set playlist id attribute
|
||||
// Set media_id attribute if metadata is provided
|
||||
if (meta) {
|
||||
att = document.createAttribute("media_id");
|
||||
att.value = meta[0];
|
||||
elem.setAttributeNode(att);
|
||||
attribute = document.createAttribute("media_id");
|
||||
attribute.value = meta[0];
|
||||
elem.setAttributeNode(attribute);
|
||||
}
|
||||
// Get filename
|
||||
elem.innerText = meta[1];
|
||||
return elem;
|
||||
};
|
||||
}
|
||||
// Bouttons de commande
|
||||
addEventListener("DOMContentLoaded", function() {
|
||||
sendCmd("/scan");
|
||||
sendCmd("/all/list");
|
||||
sendCmd("/browse_local");
|
||||
setTimeout(sendCmd, 3000, "/all/browse");
|
||||
setTimeout(sendCmd, 4000, "/all/rssi");
|
||||
adjustTl();
|
||||
//~ send_ajax_cmd("/scan");
|
||||
//~ send_ajax_cmd("/browse_local");
|
||||
//~ setTimeout(send_ajax_cmd, 3000, "/all/browse");
|
||||
//~ setTimeout(send_ajax_cmd, 4000, "/all/rssi");
|
||||
//~ setTimeout(send_ajax_cmd, 1000, "/all/list");
|
||||
adjust_timeline();
|
||||
|
||||
// Get filename when selected in table
|
||||
//~ 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"
|
||||
// Get all elements with class 'command'
|
||||
var commandButtons = document.querySelectorAll(".command");
|
||||
for (var i=0, l=commandButtons.length; i<l; i++) {
|
||||
var button = commandButtons[i];
|
||||
// Sur un click
|
||||
// Listen for clicks
|
||||
button.addEventListener("click", function(event) {
|
||||
// On intercepte le signal
|
||||
// Discard signal
|
||||
event.preventDefault();
|
||||
// On recupere la valeur de value="" sur le bouton
|
||||
// Get value=""
|
||||
var clickedButton = event.currentTarget;
|
||||
var command = clickedButton.value;
|
||||
// Proceed accordingly
|
||||
if ( command.indexOf("/reboot" ) > -1 || command.indexOf("/poweroff") > -1 ) {
|
||||
if ( !confirm("Êtes vous certain de vouloir effectuer cette action ?") ) {
|
||||
return 0;
|
||||
|
@ -149,120 +191,118 @@ addEventListener("DOMContentLoaded", function() {
|
|||
|
||||
} 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<l; i++) {
|
||||
// Get filename, time/length
|
||||
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;
|
||||
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 {
|
||||
document.getElementById("status_"+infos_array[i].host).innerHTML = "<br><br>";
|
||||
}
|
||||
// Toggle loop indicator
|
||||
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 {
|
||||
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
|
||||
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 {
|
||||
document.getElementById("repeat_ind_" + infos_array[i].host).style.backgroundColor = "#A42000"
|
||||
};
|
||||
};
|
||||
break;
|
||||
case "/all/list":
|
||||
|
||||
document.getElementById("repeat_ind_" + infos_array[i].host).style.backgroundColor = "#A42000";
|
||||
}
|
||||
}
|
||||
} 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<l; i++) {
|
||||
// Fill Playlist infos
|
||||
// Playlist infos are displayed in a div ; number of items in list and total duration.
|
||||
//~ 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
|
||||
// Populate timeline according to the content of the playlist
|
||||
// Get returned items as an array
|
||||
var items_array = Array.from(infos_array[i].items);
|
||||
//console.log(items_array.length);
|
||||
// If playlist is empty, remove all media divs in the timeline UI.
|
||||
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]);
|
||||
};
|
||||
var child_list = Array.from(document.getElementById("timeline_" + infos_array[i].host).children);
|
||||
for(x=0,y=child_list.length;x<y;x++){
|
||||
document.getElementById("timeline_" + infos_array[i].host).removeChild(child_list[x]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
//~ var html_table = "<table>" +
|
||||
//~ "<tr>" +
|
||||
//~ "<th>Id</th>" +
|
||||
//~ "<th>Filename</th>" +
|
||||
//~ "<th>Duration</th>" +
|
||||
//~ "</tr>";
|
||||
for (var j = 0, k=items_array.length; j<k; j++) {
|
||||
// Table
|
||||
item_meta = items_array[j].split(';');
|
||||
//~ html_table += "<tr>" +
|
||||
//~ "<td>" + item_meta[0] + "</td>" +
|
||||
//~ "<td>" + item_meta[1] + "</td>" +
|
||||
//~ "<td>" + item_meta[2] + "</td>" +
|
||||
//~ "</tr>" ;
|
||||
// Timeline
|
||||
var child_node = addElement("div", tl_drag_attr, item_meta, j);
|
||||
var len = document.getElementById("timeline").children.length;
|
||||
addAttr("timeline", "length", len);
|
||||
item_meta = items_array[j].split(';');
|
||||
var child_node = add_HTML_element("div", tl_drag_attr, item_meta, j);
|
||||
var len = document.getElementById("timeline_" + infos_array[i].host).children.length;
|
||||
add_HTML_attr("timeline_" + infos_array[i].host, "length", len);
|
||||
if ( len < items_array.length ) {
|
||||
document.getElementById("timeline").appendChild( addElement("div", tl_cont_attr, 0, len) );
|
||||
document.getElementById("timeline_" + infos_array[i].host).appendChild( add_HTML_element("div", tl_cont_attr, 0, len) );
|
||||
}
|
||||
document.getElementById(tl_cont_attr.id + j).replaceChildren(child_node);
|
||||
var media_name = document.getElementById(tl_cont_attr.id + j).children[0].innerText;
|
||||
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.backgroundImage = media_url_str;
|
||||
// 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 + ")";
|
||||
adjust_timeline();
|
||||
}
|
||||
}
|
||||
//~ html_table += "</table>";
|
||||
//~ document.getElementById("playlist_"+infos_array[i].host).innerHTML += html_table;
|
||||
};
|
||||
break;
|
||||
case "/scan":
|
||||
}
|
||||
} else if ( command == "/scan") {
|
||||
var host_up = infos_array[0];
|
||||
var host_down = infos_array[1];
|
||||
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';
|
||||
}
|
||||
if (host_up.length) {
|
||||
scanInterval = 10000;
|
||||
//~ document.getElementById("status_all").innerHTML = "Scan interval set to " + scanInterval;
|
||||
status_all = "Scan interval set to " + scanInterval;
|
||||
scan_interval = 10000;
|
||||
//~ document.getElementById("status_all").innerHTML = "Scan interval set to " + scan_interval;
|
||||
status_all = "Scan interval set to " + scan_interval;
|
||||
}
|
||||
//~ document.getElementById("status_all").innerHTML = host_up.length + " client(s).";
|
||||
status_all = host_up.length + " client(s).";
|
||||
break;
|
||||
case "/browse_local":
|
||||
} else if ( command == "/browse_local") {
|
||||
// Display local media files in a table
|
||||
var html_table = "<table>" +
|
||||
"<tr>" +
|
||||
"<th>Filename</th>" +
|
||||
|
@ -293,8 +333,8 @@ function parseResult(command, infos_array) {
|
|||
}
|
||||
html_table += "</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; i<l;i++) {
|
||||
document.getElementById("wl_"+i).style.backgroundColor = "hsl(0, 0%, 65%)";
|
||||
};
|
||||
}
|
||||
// Color it
|
||||
for (i=0, l=rssi_norm>4?4:rssi_norm; i<l;i++) {
|
||||
document.getElementById("wl_"+i).style.backgroundColor = "hsl(" + signal_color + ", 100%, 50%)";
|
||||
};
|
||||
};
|
||||
break;
|
||||
case "/all/browse":
|
||||
}
|
||||
}
|
||||
} else if ( command == "/all/browse") {
|
||||
// Display remote media files in a table
|
||||
for (var i=0, l=infos_array.length; i<l; i++) {
|
||||
hosts = Object.keys(infos_array[i]);
|
||||
//~ 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++ ) {
|
||||
keys_ = Object.keys(infos_array[i][hosts[j]]);
|
||||
//~ console.log(infos_array[i][hosts[j]]);
|
||||
for ( var m=0, n=keys_.length;m<n;m++ ) {
|
||||
item_meta = infos_array[i][hosts[j]][keys_[m]];
|
||||
//~ 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");
|
||||
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 += "</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.
|
||||
if (!freeze_timeline_update) {
|
||||
var infos_array = JSON.parse(request.responseText);
|
||||
//console.log(infos_array);
|
||||
parseResult(command, infos_array);
|
||||
//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);
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -61,11 +61,12 @@
|
|||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<button value="/{{host}}/browse" class="command btn btn-block btn-lg btn-default" role="button">↺<span class="btn_txt">{{gui_l10n['str_refresh']}}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right_col">
|
||||
<div id="timeline">
|
||||
<div class="timeline" id="timeline_{{host}}">
|
||||
<!--
|
||||
<div id="tl_contX"></div>
|
||||
-->
|
||||
|
|
Loading…
Reference in New Issue