From a75baea8d2400fa96896893ba57db525e6869fd0 Mon Sep 17 00:00:00 2001 From: ABelliqueux Date: Mon, 7 Nov 2022 13:27:45 +0100 Subject: [PATCH] Code restructuration, rssi is XML --- app.py | 368 +++++++++++++++------------- defaults.toml | 2 +- locales/fr/LC_MESSAGES/template.mo | Bin 1180 -> 1244 bytes locales/fr/LC_MESSAGES/template.pot | 4 + pilpil-server.toml | 6 +- 5 files changed, 202 insertions(+), 178 deletions(-) diff --git a/app.py b/app.py index c1faec4..85733ad 100755 --- a/app.py +++ b/app.py @@ -80,15 +80,15 @@ debug = app.config['DEFAULT']['debug'] media_folder_remote = app.config['DEFAULT']['media_folder_remote'] media_folder_local = os.path.expanduser(app.config['DEFAULT']['media_folder_local']) media_exts = app.config['DEFAULT']['media_exts'] -auth = str(base64.b64encode(str(":" + app.config['DEFAULT']['auth']).encode('utf-8')), 'utf-8') +auth = str(base64.b64encode(str(":" + app.config['DEFAULT']['vlc_auth']).encode('utf-8')), 'utf-8') cmd_auth = str(base64.b64encode(str(":" + app.config['DEFAULT']['cmd_auth']).encode('utf-8')), 'utf-8') hosts = app.config['DEFAULT']['hosts'] -port = app.config['DEFAULT']['port'] +vlc_port = app.config['DEFAULT']['vlc_port'] cmd_port = app.config['DEFAULT']['cmd_port'] useSSL = app.config['DEFAULT']['useSSL'] CAfile = app.config['DEFAULT']['CAfile'] sync_facility = app.config['DEFAULT']['sync_facility'] -headers = {"Authorization":"Basic " + auth} +http_headers = {"Authorization":"Basic " + auth} # SSl context creation should be out of class sslcontext = ssl.create_default_context() @@ -128,31 +128,30 @@ class PilpilClient: # Network/link utilities # https://www.metageek.com/training/resources/understanding-rssi/ -# Rewrite using HTTPConnection -def isup(listed_host, port): +def send_HTTP_request(listed_host, port, time_out=3, request_="/"): ''' - Check listed_host is up and listening on port by opening a socket - Return 1 if successfull. + Send a http request with http auth ''' - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if useSSL: - # ~ sslcontext = ssl.create_default_context() - # Disable cert validation, we just want to know if host is live - sslcontext.check_hostname = False - sslcontext.verify_mode = ssl.CERT_NONE - s = sslcontext.wrap_socket(s, server_hostname=listed_host) + 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 ) try: - s.settimeout(3.0) - s.connect((listed_host, port)) if debug: - print(_("Port {} reachable on {}").format(str(port), str(listed_host))) - return 1 - except (socket.error, socket.timeout) as e: + print(request_ + " - " + str(http_headers)) + conn.request("GET", request_, headers=http_headers) + resp = conn.getresponse() + data = resp.read() + if debug: + print(_("{} reachable on {}").format(str(listed_host), str(port))) + print("Data length:" + str(len(data))) + return data + except Exception as e: if debug: print(_("Error on connection to {} : {} : {} ").format(listed_host, str(port), e)) return 0 finally: - s.close() + conn.close() def check_hosts(host_list): ''' @@ -161,31 +160,14 @@ def check_hosts(host_list): hosts_up, hosts_down = [], [] hosts_number = str(len(host_list)) for local_host in host_list: - if not isup(local_host, port): - hosts_down.append(local_host) - else: + if send_HTTP_request(local_host, vlc_port, time_out=1): hosts_up.append(local_host) + else: + hosts_down.append(local_host) if debug: print( _("{} of {} hosts found.").format(str(len(hosts_up)), hosts_number)) return hosts_up, hosts_down -# File utilities - -def list_media_files(folder): - ''' - List files in folder which extension is allowed (exists in media_exts). - ''' - if os.path.exists(folder): - files = os.listdir(folder); - medias = [] - for fd in files: - if len(fd.split('.')) > 1: - if fd.split('.')[1] in media_exts: - medias.append(fd) - return medias - else: - return [] - def HTTP_upload(filename, host_local, port, trailing_slash=1): ''' Build HTTP file upload request and send it. @@ -196,7 +178,7 @@ def HTTP_upload(filename, host_local, port, trailing_slash=1): files = { "file":( filename, open( media_folder_local + filename, "rb"), "multipart/form-data") } if debug: print(files) - resp = requests.post(url, files=files, headers=headers, verify=CAfile) + resp = requests.post(url, files=files, headers=http_headers, verify=CAfile) if debug: print(resp.text) if resp.ok: @@ -246,6 +228,163 @@ def sync_media_folder(media_folder_local, media_folder_remote, host_local, port, total_size = "N/A"; return total_size +def get_meta_data(host, xml_data, request_="status", m3u_=0): + ''' + Parse XML response from pilpil-client instance and return a dict of metadata according to request type. + ''' + # Basic metadata + media_infos = { + 'host': host, + 'status': 0 + } + if request_ == "list": + # Return current instance's playlist + return get_playlist(host, xml_data, m3u_) + elif request_ == "status": + # Return current instance's status ( currently playing, state, time length, etc. ) + if xml_data.findall("./information/category/"): + for leaf in xml_data.findall("./information/category/"): + if leaf.get("name") == "filename": + filename = leaf.text + else: + filename = "N/A" + cur_length = int(xml_data.find('length').text) + cur_time = int(xml_data.find('time').text) + cur_length_fmtd = sec2min(cur_length) + cur_time_fmtd = sec2min(cur_time) + cur_id = int(xml_data.find('currentplid').text) + cur_pos = xml_data.find('position').text + cur_loop = xml_data.find('loop').text + cur_repeat = xml_data.find('repeat').text + media_infos.update({ + 'status' : 1, + 'file': filename, + 'time': cur_time_fmtd, + 'leng': cur_length_fmtd, + 'pos': cur_pos, + 'loop': cur_loop, + 'repeat': cur_repeat, + 'id': cur_id, + }) + elif request_ == "rssi": + # # Return current instance's wifi signal quality + print(xml_data) + if xml_data.findall("rssi"): + media_infos.update({ + 'status': 1, + 'rssi' : xml_data.find('rssi').text + }) + + return media_infos + +def get_playlist(host, xml_data, m3u=0): + playlist = [] + item_list = [] + playlist_duration = 0 + + if xml_data.find("./node") and xml_data.find("./node").get('name') == "Playlist": + playlist = xml_data.findall("./node/leaf") + + content_format = "{};{};{};" + + if m3u: + m3u_hdr = "#EXTM3U\n" + m3u_prefix = "#EXTINF:" + m3u_playlist = m3u_hdr + # M3U file building + m3u_format = "{}{}, {}\n{}\n" + m3u_content = m3u_hdr + + for item in playlist: + # item info + if m3u: + m3u_content += m3u_format.format(m3u_prefix, item.get("duration"), item.get("name"), item.get("uri")) + item_info = content_format.format(item.get("id"), item.get("name"), sec2min(int(item.get("duration")))) + # Add cursor to currently playing element + if "current" in item.keys(): + item_info += item.get("current") + item_list.append(item_info) + # Compute playlist length + playlist_duration += int(item.get("duration")) + if debug: + if m3u: + print(m3u_content) + playlist_overview = { + 'host' : host, + 'status': 1, + '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. + ''' + port_ = vlc_port + # Build request + # + # Default request + HTTP_request = "/requests/status.xml" + if arg0 == "list" : + # Get playlist + HTTP_request = "/requests/playlist.xml" + elif arg0 in cmd_server: + # Switching to cmd server + HTTP_request = "/" + str(arg0) + port_ = cmd_port + elif arg0 != "status" : + # Build request for VLC command + HTTP_request = HTTP_request + "?command=" + cmd_player[arg0] + 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 + # https://github.com/videolan/vlc/blob/3.0.17.4/modules/lua/libs/playlist.c#L353-L362 + HTTP_request = HTTP_request + "&val=" + escape_str(arg2) + elif arg0 == "seek" : + HTTP_request = HTTP_request + "&val=" + arg1 + elif (arg0 == "enqueue") or (arg0 == "add") : + # Add 'input' url parameter + HTTP_request = HTTP_request + "&input=file://" + media_folder_remote + "/" + 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 not data: + print("No data was received.") + return 0 + + # Parse xml data + xml = ET.fromstring(data) + # Process parsed data and return dict + metadata = get_meta_data(host, xml, arg0) + if debug: + print("Metadata:" + str(metadata)) + return metadata + +# Utilities +def list_media_files(folder): + ''' + List files in folder which extension is allowed (exists in media_exts). + ''' + if os.path.exists(folder): + files = os.listdir(folder); + medias = [] + for fd in files: + if len(fd.split('.')) > 1: + if fd.split('.')[1] in media_exts: + medias.append(fd) + return medias + else: + return [] # /requests/status.xml?command=in_enqueue&input=file:///home/pi/tst1.mp4 def write_M3U(m3u_content : str, host : str): ''' @@ -256,8 +395,6 @@ def write_M3U(m3u_content : str, host : str): fd.write(m3u_content) fd.close() return 1 - -# String utilities def escape_str(uri): ''' @@ -271,128 +408,6 @@ def sec2min(duration): ''' return('%02d:%02d' % (duration / 60, duration % 60)) -# VLC lua utilities -# TODO : make that a class -def send_command(host, arg0, arg1, arg2): - ''' - Build a http request according to args, send it and return parsed result. - ''' - portl = port - # Build request - req = "/requests/status.xml" - if arg0 == "list" : - req = "/requests/playlist.xml" - elif arg0 in cmd_server: - req = "/" + str(arg0) - portl = cmd_port - elif arg0 != "status" : - req = req + "?command=" + cmd_player[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=" + escape_str(arg2) - elif arg0 == "seek" : - req = req + "&val=" + arg1 - elif (arg0 == "enqueue") or (arg0 == "add") : - req = req + "&input=file://" + media_folder_remote + "/" + arg1 - # Send request - if useSSL: - # ~ sslcontext = ssl.create_default_context() - # ~ if os.path.exists(CAfile): - # ~ sslcontext.load_verify_locations(cafile=CAfile) - # ~ else: - # ~ sslcontext.check_hostname = False - # ~ sslcontext.verify_mode = ssl.CERT_NONE - conn = http.client.HTTPSConnection( host + ":" + str(portl), timeout=3, context = sslcontext ) - else: - conn = http.client.HTTPConnection( host + ":" + str(portl), timeout=3 ) - try: - conn.request( "GET", req, headers = headers ) - resp = conn.getresponse() - data = resp.read() - except http.client.HTTPException: - print( _("Connection to {} timed out").format(host) ) - return _("Connection to {} timed out").format(host) - except: - return _("Error while connecting to {}:{}").format(host, str(portl)) - finally: - conn.close() - # 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) - cur_id = int(xml.find('currentplid').text) - cur_pos = xml.find('position').text - cur_loop = xml.find('loop').text - cur_repeat = xml.find('repeat').text - response_dict = { - 'host': host, - 'file': filename, - 'time': cur_time_fmtd, - 'leng': cur_length_fmtd, - 'pos': cur_pos, - 'loop': cur_loop, - 'repeat': cur_repeat, - # ~ 'pos': xml.find('position').text, - # ~ 'loop': xml.find('loop').text, - # ~ 'repeat': xml.find('repeat').text, - 'id': cur_id, - } - 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") ) ) + ";" - if "current" in item.keys(): - item_info += item.get("current") - 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("/") @@ -409,43 +424,48 @@ def main(): def scan(): global hosts_available, hosts_unavailable hosts_available, hosts_unavailable = check_hosts(hosts) - hosts_status = [hosts_available, hosts_unavailable] - return hosts_status + return [hosts_available, hosts_unavailable] @app.route("/browse") def browse(): - files = list_media_files(media_folder_local) - return files; + return list_media_files(media_folder_local) - @app.route("/sync/") def sync(host): + # TODO : Add feedback for transfer in GUI + size = 0 if host == "all": for hostl in hosts_available: - size = sync_media_folder(media_folder_local, media_folder_remote, hostl, cmd_port) + size += sync_media_folder(media_folder_local, media_folder_remote, hostl, cmd_port) else: size = sync_media_folder(media_folder_local, media_folder_remote, host, cmd_port) return size; - @app.route("///", defaults = { "arg1": "null", "arg2": "null" }) @app.route("////", defaults = { "arg2": "null" }) @app.route("////") def action(host, arg0, arg1, arg2): status_message = "Idle" + if (arg0 not in cmd_player) and (arg0 not in cmd_server): status_message = "

{}

".format(_("Wrong command")) - elif host == "all": + return status_message + + if host == "all": + # Send request to all available hosts resp = [] for hostl in hosts_available: - resp.append( send_command(hostl, arg0, arg1, arg2) ) + resp.append(send_pilpil_command(hostl, arg0, arg1, arg2)) status_message = resp elif host not in hosts_available: status_message = "

{}

".format("Host is not reachable") else: - status_message = send_command(host, arg0, arg1, arg2) + # Send request to specified host + status_message = send_pilpil_command(host, arg0, arg1, arg2) + if debug: print(status_message) + return status_message if __name__ == '__main__': diff --git a/defaults.toml b/defaults.toml index 5fcf2ee..3cbde6f 100644 --- a/defaults.toml +++ b/defaults.toml @@ -11,6 +11,6 @@ auth = "secret" cmd_auth = "secret" hosts = [] # VLC http LUA port -port = 0 +vlc_port = 0 # Clients cmd port cmd_port = 0 \ No newline at end of file diff --git a/locales/fr/LC_MESSAGES/template.mo b/locales/fr/LC_MESSAGES/template.mo index 172571df5c12fbc82071f6e67245c7a05ddec94f..3655f6ab629f5bfcfc465d642ac2d9f2defa85c7 100644 GIT binary patch delta 383 zcmXZXu}{K46vy$`TEqeptzaT)pou0LqhU93*3PUfrItuSFri^;2#X5|t1j{nkht+j zz~abe7}?wz82rA5m;Cy<++FY8P3>QMw05iyp^C&M6LLm&MC;iQsi2Qn_<}VYV-;s; z;S#g>gD1GcQ{3PsZt)mhQ^dhrEQ^ffaX-<(5}Vqn4Uz+qJN!azyu=&)LmSWOs+acg z0_&&;`*@8_)C;^LjpP&cA>UZS1sd|ih$;|0G($K)eF>zcP4fG(z92{VSGZ?dhkTFb p_eRlhlA+g+g77tHbtCVs=Ox4WgV{36%m-l@#c@X$;z6Gk`3KU|C^rBA delta 318 zcmXBPy$%6E6o%n5Yy7OA4I2s~t3W6uY^~NQv`URe!xd~KZa|^q9wZy3#s#