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

308
app.py
View File

@ -1,5 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
# pilpil-client 0.1 # pilpil-client 0.1
# abelliqueux <contact@arthus.net> # abelliqueux <contact@arthus.net>
import base64 import base64
@ -7,41 +7,39 @@ 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
from waitress import serve from waitress import serve
# l10n # l10n
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,53 +47,53 @@ 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",
"reboot", "reboot",
"poweroff", "poweroff",
"rssi", "rssi",
"sync" "sync"
] ]
# Configuration # Configuration
debug = app.config['DEFAULT']['debug'] debug = app.config['DEFAULT']['debug']
pi_user = app.config['DEFAULT']['pi_user'] pi_user = app.config['DEFAULT']['pi_user']
media_folder_remote = app.config['DEFAULT']['media_folder_remote'] media_folder_remote = app.config['DEFAULT']['media_folder_remote']
@ -110,44 +108,18 @@ cmd_port = app.config['DEFAULT']['cmd_port']
useSSL = app.config['DEFAULT']['useSSL'] 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,18 +197,18 @@ 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:
current_upload = media.strip("/") current_upload = media.strip("/")
if debug: if debug:
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))
# ~ return _("{} files uploaded.").format(str(transfer_ok)) # ~ return _("{} files uploaded.").format(str(transfer_ok))
# Using system cmd # Using system cmd
@ -244,41 +219,45 @@ def sync_media_folder(media_folder_local, media_folder_remote, host_local, port,
scrape_str = "total size is " 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.
''' '''
# Basic metadata # Basic metadata
media_infos = { media_infos = {
'host': host, 'host': host,
'status': 0 'status': 0
} }
if request_ == "list": if request_ == "list":
# 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,10 +269,10 @@ 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,
'pos': cur_pos, 'pos': cur_pos,
'loop': cur_loop, 'loop': cur_loop,
'repeat': cur_repeat, 'repeat': cur_repeat,
@ -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 = {}
@ -314,26 +293,24 @@ def get_meta_data(host, xml_data, request_="status", m3u_=0):
media_name = media.attrib["name"] media_name = media.attrib["name"]
if len(media_name.split('.')) > 1: if len(media_name.split('.')) > 1:
if media_name.split('.')[1] in media_exts: if media_name.split('.')[1] in media_exts:
host_medias[media_name] = { host_medias[media_name] = {
"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:"
@ -341,13 +318,13 @@ def get_playlist(host, xml_data, m3u=0):
# M3U file building # M3U file building
m3u_format = "{}{}, {}\n{}\n" m3u_format = "{}{}, {}\n{}\n"
m3u_content = m3u_hdr m3u_content = m3u_hdr
for item in playlist: for item in playlist:
# item info # item info
if m3u: if m3u:
m3u_content += m3u_format.format(m3u_prefix, item.get("duration"), item.get("name"), item.get("uri")) m3u_content += m3u_format.format(m3u_prefix, item.get("duration"), item.get("name"), item.get("uri"))
item_info = content_format.format(item.get("id"), item.get("name"), sec2min(int(item.get("duration")))) item_info = content_format.format(item.get("id"), item.get("name"), sec2min(int(item.get("duration"))))
# Add cursor to currently playing element # Add cursor to currently playing element
if "current" in item.keys(): if "current" in item.keys():
item_info += item.get("current") item_info += item.get("current")
item_list.append(item_info) item_list.append(item_info)
@ -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,29 +372,31 @@ 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
# Parse xml data # Parse xml data
xml = ET.fromstring(data) xml = ET.fromstring(data)
# Process parsed data and return dict # Process parsed data and return dict
@ -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
''' '''
@ -450,67 +433,73 @@ def write_M3U(m3u_content : str, host : str):
fd.write(m3u_content) fd.write(m3u_content)
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,)) else:
# ~ th.start()
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":
# Send request to all available hosts # Send request to all available hosts
responses = [] responses = []
@ -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
@ -57,31 +70,29 @@ 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
# OTHER: * webgui: l10n
* get_client_rssi.sh on server * webgui: Add remote file enqueuing
* webgui: Fix timeline UI
* webgui: Add video thumbnails to timeline UI
* client: Only blink when running on rpi
* client: Generate video thumbnail on start with ffmpegthumbnailer, serve SVG when file not found
* server: pep8fied (except for line length)
# TODO : # 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() { function adjust_timeline() {
var child = document.getElementById('timeline').children; // 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,169 +150,159 @@ 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;
} }
} else if ( command == "/scan" ) { } else if ( command == "/scan" ) {
document.getElementById("status_all").innerHTML = status_all; document.getElementById("status_all").innerHTML = status_all;
} 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); }
// Adjust elements width for (var j = 0, k=items_array.length; j<k; j++) {
adjustTl(); // Timeline
// Highlight currently playing element item_meta = items_array[j].split(';');
if (item_meta[3] != ""){ var child_node = add_HTML_element("div", tl_drag_attr, item_meta, j);
document.getElementById(tl_cont_attr.id + j).children[0].style.borderBottom = "4px solid " + timeline_color_cursor; var len = document.getElementById("timeline_" + infos_array[i].host).children.length;
document.getElementById(tl_cont_attr.id + j).children[0].style.fontWeight = "bold"; add_HTML_attr("timeline_" + infos_array[i].host, "length", len);
var pos = medias_status[item_meta[0]] * 100; if ( len < items_array.length ) {
//~ pos = pos.toPrecision(2); document.getElementById("timeline_" + infos_array[i].host).appendChild( add_HTML_element("div", tl_cont_attr, 0, len) );
var pos1 = pos-1 + "%"; }
pos = pos + "%"; document.getElementById(tl_cont_attr.id + j).replaceChildren(child_node);
//console.log( "linear-gradient(90deg," + timeline_color2 + " " + pos1 + ", " + timeline_color1 + " " + pos + ", " + timeline_color2 + " " + pos + ")" ); var media_name = document.getElementById(tl_cont_attr.id + j).children[0].innerText;
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 + ")"; 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
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>
--> -->