From b51b2db8db7b7437a1a586ab14322f0afebfbebe Mon Sep 17 00:00:00 2001 From: ABelliqueux Date: Thu, 6 Oct 2022 11:44:59 +0200 Subject: [PATCH] init --- app.py | 276 ++++++++++++++++++++++++++++++++++++++++++++ static/script.js | 177 ++++++++++++++++++++++++++++ static/style.css | 76 ++++++++++++ templates/main.html | 75 ++++++++++++ 4 files changed, 604 insertions(+) create mode 100755 app.py create mode 100644 static/script.js create mode 100644 static/style.css create mode 100644 templates/main.html diff --git a/app.py b/app.py new file mode 100755 index 0000000..b09d0d0 --- /dev/null +++ b/app.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +import sys, os +import http.client +import xml.etree.ElementTree as ET +from flask import Flask, render_template, request, make_response, jsonify +from waitress import serve + +app = Flask(__name__) + +DEBUG = 0 +video_folder = "/home/pi" +media_ext = [ "mp4", "avi", "mkv" ] +# ~ video_folder = "/media/" +## base64 encoded ":secret"" +# import base64 +# passwd = "foo" +# passswd64 = str(base64.b64encode(passwd.encode('utf-8')), 'utf-8') +auth = "OnNlY3JldA==" +cmd_auth = "OnNlY3JldA==" +hosts = [ "10.42.0.135", "10.42.0.156" ] +# VLC http LUA port +port = 8080 +# Clients cmd port +cmd_port = 5000 + +hosts_available, hosts_unavailable = [],[] + +# Map vlc cmds +# See https://github.com/videolan/vlc/blob/1336447566c0190c42a1926464fa1ad2e59adc4f/share/lua/http/requests/README.txt +cmd = { + "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", + "sync" : "sync", + "status" : "status.xml", + "list" : "playlist.xml", + #"volume" : "volume", + #"ratio" : "aspectratio", + #"dir" : "?dir=", + #"command" : "?command=", + #"key" : "key=", + #"browse" : "browse.xml?uri=file://~" +# System commands : + "rssi" : "rssi", + "blink" : "blink", + "poweroff" : "poweroff", + "reboot" : "reboot", + +} + +# Network/link utilities +# https://www.metageek.com/training/resources/understanding-rssi/ + +def isup(host_l, port): + global DEBUG + import socket + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.settimeout(3.0) + s.connect((host_l, port)) + if DEBUG: + print( "Port " + str(port) + " reachable") + return 1 + except (socket.error, socket.timeout) as e: + if DEBUG: + print("Error on connect to " + host_l + ": %s" % e) + return 0 + finally: + s.close() + +def checkHosts(host_l): + hostdown, hostup = [], [] + hosts_number = str(len(host_l)) + for lhost in host_l: + if not isup(lhost, 8080): + hostdown.append(lhost) + else: + hostup.append(lhost) + if DEBUG: + print( str(len(hostup)) + " of " + hosts_number + " hosts found.") + return hostup, hostdown + +# File utilities + +def listMediaFiles(folder): + files = os.listdir(folder); + medias = [] + for fd in files: + if len(fd.split('.')) > 1: + if fd.split('.')[1] in media_ext: + medias.append(fd) + return medias + +# /requests/status.xml?command=in_enqueue&input=file:///home/pi/tst1.mp4 +def writeM3U(m3u_content : str, host : str): + filename = host.replace(".", "_") + ".m3u" + fd = open(filename, "w") + fd.write(m3u_content) + fd.close() + return 1 + +# String utilities + +def escapeStr(uri): + return uri.replace(" ", "%20") + +def sec2min(duration): + return('%02d:%02d' % (duration / 60, duration % 60)) + +# VLC lua utilities + +def sendCommand(host, arg0, arg1, arg2): + #conn = http.client.HTTPConnection( host + ":" + str(port), timeout=3 ) + portl = port + # Build request + req = "/requests/status.xml" + if arg0 == "list" : + req = "/requests/playlist.xml" + elif arg0 == "rssi": + req = "/rssi" + portl = cmd_port + elif arg0 == "reboot": + req = "/reboot" + portl = cmd_port + elif arg0 == "poweroff": + req = "/poweroff" + portl = cmd_port + elif arg0 != "status" : + req = req + "?command=" + cmd[arg0] + if arg1 != "null" : + if (arg0 == "play") or (arg0 == "delete") or (arg0 == "sort") or (arg0 == "move"): + req = req + "&id=" + arg1 + if (arg0 == "sort") or (arg0 == "move") : + # 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 + req = req + "&val=" + escapeStr(arg2) + elif arg0 == "seek" : + req = req + "&val=" + arg1 + elif (arg0 == "enqueue") or (arg0 == "add") : + req = req + "&input=file://" + video_folder + "/" + arg1 + # Send request + conn = http.client.HTTPConnection( host + ":" + str(portl), timeout=3 ) + try: + conn.request( "GET", req, headers={"Authorization":"Basic " + auth} ) + except: + return "Connection to " + host + " was refused on port " + str(portl) + resp = conn.getresponse() + # Parse response + data = resp.read() + + if arg0 == "rssi": + if DEBUG: + print(data) + response_dict = { + 'host': host, + 'rssi': str(data, 'UTF-8') + } + return response_dict + + xml = ET.fromstring(data) + + if arg0 != "list" : + meta = xml.findall("./information/category/") + if meta: + for leaf in meta: + if leaf.get("name") == "filename": + filename = leaf.text + else: + filename = "N/A" + cur_length = int(xml.find('length').text) + cur_time = int(xml.find('time').text) + cur_length_fmtd = sec2min(cur_length) + cur_time_fmtd = sec2min(cur_time) + response_dict = { + 'host': host, + 'file': filename, + 'time': cur_time_fmtd, + 'leng': cur_length_fmtd, + 'pos': xml.find('position').text, + 'loop': xml.find('loop').text, + 'repeat': xml.find('repeat').text, + } + return response_dict + + else: + # Build M3U file from playlist + playlist = [] + playlist_duration = 0 + m3u_hdr = "#EXTM3U\n" + m3u_prefix = "#EXTINF:" + item_list = [] + if xml.find("./node") and xml.find("./node").get('name') == "Playlist": + m3u_content = m3u_hdr + playlist = xml.findall("./node/leaf") + #item_list = [] + for item in playlist: + m3u_content += m3u_prefix + item.get("duration") + ", " + item.get("name") + "\n" + item.get("uri") + "\n" + playlist_duration += int(item.get("duration")) + # ~ item_info = item.get("id") + " : " + item.get("name") + " - " + sec2min( int( item.get("duration") ) ) + item_info = item.get("id") + ";" + item.get("name") + ";" + sec2min( int( item.get("duration") ) ) + item_list.append(item_info) + + playlist_overview = { + "host" : host, + "leng" : str(len(playlist)), + "duration" : sec2min(playlist_duration), + "items" : item_list + } + return playlist_overview + +status_message = "Idle" + +@app.route("/") +def main(): + global hosts + status_message = "Searching network for live hosts..." + # ~ hosts_available, hosts_unavailable = checkHosts(hosts) + templateData = { + 'hosts' : hosts, + 'status_message' : status_message + } + return render_template('main.html', **templateData) + +@app.route("/scan") +def scan(): + global hosts_available, hosts_unavailable + hosts_available, hosts_unavailable = checkHosts(hosts) + hosts_status = [hosts_available, hosts_unavailable] + return hosts_status + +@app.route("/browse") +def browse(): + files = listMediaFiles("../"); + return files; + + +@app.route("///", defaults = { "arg1": "null", "arg2": "null" }) +@app.route("////", defaults = { "arg2": "null" }) +@app.route("////") +def action(host, arg0, arg1, arg2): + status_message = "Idle" + if arg0 not in cmd: + # ~ return "

Wrong command

" + status_message = "

Wrong command

" + elif host == "all": + resp = [] + for hostl in hosts_available: + resp.append( sendCommand(hostl, arg0, arg1, arg2) ) + status_message = resp + elif host not in hosts_available: + status_message = "

Host is not reachable

" + else: + status_message = sendCommand(host, arg0, arg1, arg2) + if DEBUG: + print(status_message) + return status_message + +if __name__ == '__main__': + app.run() + # ~ serve(app, host='127.0.0.1', port=8080) \ No newline at end of file diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..d321067 --- /dev/null +++ b/static/script.js @@ -0,0 +1,177 @@ +var scanInterval = 3000; +// Bouttons de commande +addEventListener("DOMContentLoaded", function() { + sendCmd("/scan"); + sendCmd("/browse"); + sendCmd("/all/rssi"); + // Tous les elements avec la classe ".command" + var commandButtons = document.querySelectorAll(".command"); + for (var i=0, l=commandButtons.length; i -1 ){ + if (command.indexOf('/1/') > -1 ) { + clickedButton.value = clickedButton.value.replace('/1/','/0/') + } else { + clickedButton.value = clickedButton.value.replace('/0/','/1/') + } + } else if ( command.indexOf("/move") > -1 ) { + const test_array = [5,6,7,8]; + for (i=test_array.length, l=0;i>l;i--){ + console.log(test_array[i-1]); + sendCmd("/all/move/" + test_array[i-1] + "/1"); + + }; + //setInterval( sendCmd, scanInterval, "/all/move/16/1"); + }; + + // On envoie la commande en AJAX + var request = new XMLHttpRequest(); + if ( command == "/scan" ) { + request.onload = refreshInfos(command); + } + // On construit la commande + request.open("GET", command, true); + // et on l'envoie + request.send(); + }); + } +}, true); + +// Affichage des infos +function parseResult(command, infos_array) { + //~ 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. + //~ const infos_array = JSON.parse(request.responseText); + switch (command) { + case "/all/status": + // Iterate over array + for (var i = 0, l=infos_array.length; i" + + "Id" + + "Filename" + + "Duration" + + ""; + for (var j = 0, k=items_array.length; j" + item_meta[0] + "" + + "" + item_meta[1] + "" + + "" + item_meta[2] + "" + + "" ; + } + html_table += ""; + document.getElementById("playlist_"+infos_array[i].host).innerHTML += html_table; + }; + break; + case "/scan": + var host_up = infos_array[0]; + var host_down = infos_array[1]; + for ( var i=0, l=host_up.length; i" + + "Filename" + + "Duration" + + ""; + for (var j = 0, k=infos_array.length; j" + infos_array[j] + "" + + "" + "00:00" + "" + + "" ; + } + html_table += ""; + document.getElementById("filelist").innerHTML += html_table; + break; + case "/all/rssi": + var signal_color = 40; + var best_rssi = 30; + var worst_rssi = 70; + for (var j = 0, k=infos_array.length; j + + RPi Web Server + + + + + + +
+
+

Videopi commander

+

{{status_message}}

+

No files.

+
+
+

+ + + + + + + + + + + + +

+
+
+ {% for host in hosts %} +
+
+
+

{{ host }}

+ + +

{{status_message}}

+

+ Signal: + + + + +

+

Loop

+

Repeat

+
+
+

No files queued.

+
+
+
+
+

+ + + + + + + + + +

+
+
+ {% endfor %} + + +