diff --git a/app/connections.py b/app/connections.py index 12e75472..172e5f45 100644 --- a/app/connections.py +++ b/app/connections.py @@ -1,15 +1,17 @@ -import json import os +import re import socket import time import urllib +import xml.etree.ElementTree as ETree from enum import Enum from ftplib import FTP, error_perm from http.client import RemoteDisconnected from telnetlib import Telnet from urllib.error import HTTPError, URLError from urllib.parse import urlencode -from urllib.request import urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener, install_opener +from urllib.request import urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, \ + build_opener, install_opener, Request from app.commons import log from app.settings import SettingsType @@ -22,6 +24,8 @@ _DATA_FILES_LIST = ("lamedb", "lamedb5", "services.xml", "blacklist", "whitelist _SAT_XML_FILE = "satellites.xml" _WEBTV_XML_FILE = "webtv.xml" +_HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/71.0"} + class DownloadType(Enum): ALL = 0 @@ -37,8 +41,9 @@ class HttpRequestType(Enum): INFO = "about" SIGNAL = "tunersignal" STREAM = "streamcurrentm3u" - STATUS = "statusinfo" + CURRENT = "getcurrent" PLAY = "mediaplayerplay?file=4097:0:1:0:0:0:0:0:0:0:" + TEST = None class TestException(Exception): @@ -101,7 +106,7 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False s_type = settings.setting_type data_path = settings.data_local_path host = settings.host - base_url = "http{}://{}:{}/api/".format("s" if settings.http_use_ssl else "", host, settings.http_port) + base_url = "http{}://{}:{}/web/".format("s" if settings.http_use_ssl else "", host, settings.http_port) tn, ht = None, None # telnet, http try: @@ -122,7 +127,7 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False if download_type is DownloadType.ALL: time.sleep(5) - ht.send(base_url + "/powerstate?newstate=0") + ht.send(base_url + "powerstate?newstate=0") time.sleep(2) else: # telnet @@ -167,10 +172,10 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False tn.send("init 3" if s_type is SettingsType.ENIGMA_2 else "init 6") elif ht and use_http: if download_type is DownloadType.BOUQUETS: - ht.send(base_url + "/servicelistreload?mode=2") + ht.send(base_url + "servicelistreload?mode=2") elif download_type is DownloadType.ALL: - ht.send(base_url + "/servicelistreload?mode=0") - ht.send(base_url + "/powerstate?newstate=4") + ht.send(base_url + "servicelistreload?mode=0") + ht.send(base_url + "powerstate?newstate=4") if done_callback is not None: done_callback() @@ -246,10 +251,9 @@ def http(user, password, url, callback): init_auth(user, password, url) while True: url = yield - with urlopen(url, timeout=5) as f: - msg = json.loads(f.read().decode("utf-8")).get("message", None) - if msg: - callback("HTTP: {}\n".format(msg)) + msg = get_response(HttpRequestType.TEST, url).get("e2statetext", None) + if msg: + callback("HTTP: {}\n".format(msg)) def telnet(host, port=23, user="", password="", timeout=5): @@ -298,28 +302,36 @@ class HttpAPI: elif req_type is HttpRequestType.PLAY: url += urllib.parse.quote(ref).replace("%3A", "%253A") - future = self._executor.submit(get_json, req_type, url) + future = self._executor.submit(get_response, req_type, url) future.add_done_callback(lambda f: callback(f.result())) def init(self): use_ssl = self._settings.http_use_ssl url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port) - self._base_url = "{}/api/".format(url) + self._base_url = "{}/web/".format(url) init_auth(self._settings.http_user, self._settings.http_password, url, use_ssl) def close(self): self._executor.shutdown(False) -def get_json(req_type, url): +def get_response(req_type, url): try: - with urlopen(url, timeout=10) as f: + with urlopen(Request(url, headers=_HEADERS), timeout=10) as f: if req_type is HttpRequestType.STREAM: return f.read().decode("utf-8") + elif req_type is HttpRequestType.CURRENT: + for e in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"): + return {e.tag: e.text for e in e.iter()} # return first[current] event from the list else: - return json.loads(f.read().decode("utf-8")) - except (URLError, HTTPError, RemoteDisconnected): - pass + return {e.tag: e.text for e in ETree.fromstring(f.read().decode("utf-8")).iter()} + except (URLError, HTTPError, RemoteDisconnected, ConnectionResetError) as e: + if req_type is HttpRequestType.TEST: + raise e + except ETree.ParseError as e: + log("Parsing response error: {}".format(e)) + + return {} # ***************** Connections testing *******************# @@ -339,24 +351,9 @@ def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message # authentication init_auth(user, password, base_url, use_ssl) try: - with urlopen("{}/api/{}".format(base_url, params), timeout=5) as f: - return json.loads(f.read().decode("utf-8")).get("message", "") - except HTTPError as e: - if e.code == 404: - return test_api("{}/web/{}".format(base_url, params)) - raise TestException(e) - except (RemoteDisconnected, URLError) as e: - raise TestException(e) - - -def test_api(url): - """ Additional HTTP API compatibility test. """ - try: - with urlopen(url, timeout=5) as f: - pass # NOP + return get_response(HttpRequestType.TEST, "{}/web/{}".format(base_url, params)).get("e2statetext", "") except (RemoteDisconnected, URLError, HTTPError) as e: raise TestException(e) - raise HttpApiException("HTTP API is not supported yet for this receiver!") def init_auth(user, password, url, use_ssl=False): @@ -381,9 +378,12 @@ def test_telnet(host, port, user, password, timeout=5): try: gen = telnet_test(host, port, user, password, timeout) res = next(gen) - print(res) - res = next(gen) - return res + msg = str(res, encoding="utf8").strip() + log(msg) + next(gen) + if re.search("password", msg, re.IGNORECASE): + raise TestException(msg) + return msg except (socket.timeout, OSError) as e: raise TestException(e) @@ -392,14 +392,14 @@ def telnet_test(host, port, user, password, timeout): tn = Telnet(host=host, port=port, timeout=timeout) time.sleep(1) tn.read_until(b"login: ", timeout=2) - tn.write(user.encode("utf-8") + b"\n") + tn.write(user.encode("utf-8") + b"\r") time.sleep(timeout) tn.read_until(b"Password: ", timeout=2) - tn.write(password.encode("utf-8") + b"\n") + tn.write(password.encode("utf-8") + b"\r") time.sleep(timeout) yield tn.read_very_eager() tn.close() - yield "Done!" + yield if __name__ == "__main__": diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index 39aa4202..f58c9cc4 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -1,6 +1,7 @@ import os import sys from contextlib import suppress +from datetime import datetime from functools import lru_cache from itertools import chain @@ -232,6 +233,7 @@ class Application(Gtk.Application): self._save_header_button.bind_property("sensitive", builder.get_object("save_menu_button"), "sensitive") self._signal_level_bar.bind_property("visible", builder.get_object("play_current_service_button"), "visible") self._receiver_info_box.bind_property("visible", self._http_status_image, "visible", 4) + self._receiver_info_box.bind_property("visible", builder.get_object("signal_box"), "visible") # Force ctrl press event for view. Multiple selections in lists only with Space key(as in file managers)!!! self._services_view.connect("key-press-event", self.force_ctrl) self._fav_view.connect("key-press-event", self.force_ctrl) @@ -267,6 +269,7 @@ class Application(Gtk.Application): self._player_box.bind_property("visible", builder.get_object("main_popover_menu_box"), "visible", 4) self._player_box.bind_property("visible", builder.get_object("download_header_button"), "visible", 4) self._player_box.bind_property("visible", builder.get_object("left_header_separator"), "visible", 4) + self._player_box.bind_property("visible", self._profile_combo_box, "sensitive", 4) # Enabling events for the drawing area self._player_drawing_area.set_events(Gdk.ModifierType.BUTTON1_MASK) self._player_frame = builder.get_object("player_frame") @@ -1203,11 +1206,9 @@ class Application(Gtk.Application): self._s_type = self._settings.setting_type self._profile_combo_box.set_tooltip_text(self._profile_combo_box.get_tooltip_text() + self._settings.host) self.update_profile_label() - - if self._http_api and self._settings.http_api_support: - self._http_api.init() - - self.open_data() + gen = self.init_http_api() + GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) + self.open_data() def update_profiles(self): self._profile_combo_box.remove_all() @@ -1600,11 +1601,11 @@ class Application(Gtk.Application): def on_player_previous(self, item): if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, -1): - self.on_play_stream() + self.on_zap(self.on_watch) if self._fav_click_mode is FavClickMode.PLAY else self.on_play_stream() def on_player_next(self, item): if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1): - self.on_play_stream() + self.on_zap(self.on_watch) if self._fav_click_mode is FavClickMode.PLAY else self.on_play_stream() def on_player_rewind(self, scale, scroll_type, value): self._player.set_time(int(value)) @@ -1680,17 +1681,16 @@ class Application(Gtk.Application): if not state & Gdk.WindowState.ICONIFIED and self._links_transmitter: self._links_transmitter.hide() - # ************************ HTTP API ****************************# + # ************************ HTTP API **************************** # def init_http_api(self): self._fav_click_mode = FavClickMode(self._settings.fav_click_mode) http_api_enable = self._settings.http_api_support - status = all( - (http_api_enable, self._s_type is SettingsType.ENIGMA_2, not self._receiver_info_box.get_visible())) - GLib.idle_add(self._http_status_image.set_visible, status) + st = all((http_api_enable, self._s_type is SettingsType.ENIGMA_2, not self._receiver_info_box.get_visible())) + GLib.idle_add(self._http_status_image.set_visible, st) if self._s_type is SettingsType.NEUTRINO_MP or not http_api_enable: - GLib.idle_add(self.update_info_boxes_visible, False) + GLib.idle_add(self._receiver_info_box.set_visible, False) if self._http_api: self._http_api.close() yield True @@ -1701,6 +1701,8 @@ class Application(Gtk.Application): if not self._http_api: self._http_api = HttpAPI(self._settings) GLib.timeout_add_seconds(3, self.update_info, priority=GLib.PRIORITY_LOW) + else: + self._http_api.init() self.init_send_to(http_api_enable and self._settings.enable_send_to) yield True @@ -1738,7 +1740,7 @@ class Application(Gtk.Application): ref = srv.picon_id.rstrip(".png").replace("_", ":") def zap(rq): - if rq and rq.get("result", False): + if rq and rq.get("e2state", False): GLib.idle_add(scroll_to, path, self._fav_view) if callback is not None: callback() @@ -1747,49 +1749,51 @@ class Application(Gtk.Application): def update_info(self): """ Updating current info over HTTP API """ - if not self._http_api: + if not self._http_api or self._s_type is SettingsType.NEUTRINO_MP: GLib.idle_add(self._http_status_image.set_visible, False) + GLib.idle_add(self._receiver_info_box.set_visible, False) return False self._http_api.send(HttpRequestType.INFO, None, self.update_receiver_info) - self._http_api.send(HttpRequestType.INFO, None, self.update_service_info) return True def update_receiver_info(self, info): - res_info = info.get("info", None) if info else None + res_info = info.get("e2about", None) if info else None if res_info: - image = res_info.get("friendlyimagedistro", "") - image_ver = res_info.get("imagever", "") - brand = res_info.get("brand", "") - model = res_info.get("model", "") - info_text = "{} {} Image: {} {}".format(brand, model, image, image_ver) + image = info.get("e2distroversion", "") + image_ver = info.get("e2imageversion", "") + model = info.get("e2model", "") + info_text = "{} Image: {} {}".format(model, image, image_ver) GLib.idle_add(self._receiver_info_label.set_text, info_text) + GLib.idle_add(self._service_name_label.set_text, info.get("e2servicename", None) or "") + self.update_service_info(info) GLib.idle_add(self._receiver_info_box.set_visible, bool(res_info)) + @run_idle def update_service_info(self, info): - service_info = info.get("service", None) if info else None - if service_info: - GLib.idle_add(self._service_name_label.set_text, service_info.get("name", "")) - if service_info.get("onid", None) and self._http_api: - self._http_api.send(HttpRequestType.SIGNAL, None, self.update_signal) - self._http_api.send(HttpRequestType.STATUS, None, self.update_status) - GLib.idle_add(self._signal_box.set_visible, bool(service_info)) + has_onid = info.get("e2onid", "N/A") != "N/A" + if has_onid and self._http_api: + self._http_api.send(HttpRequestType.SIGNAL, None, self.update_signal) + self._http_api.send(HttpRequestType.CURRENT, None, self.update_status) + self._signal_level_bar.set_visible(has_onid) def update_signal(self, sig): - self.set_signal(sig.get("snr", 0) if sig else 0) + self.set_signal(sig.get("e2snr", "0 %") if sig else "0 %") @lru_cache(maxsize=2) def set_signal(self, val): - self._signal_level_bar.set_value(val if isinstance(val, int) else 0) - self._signal_level_bar.set_visible(val) + self._signal_level_bar.set_value(int(val.rstrip("%").strip() or 0)) - def update_status(self, status): - if status: - dsc = "{} {} - {}".format(status.get("currservice_name", ""), - status.get("currservice_begin", ""), - status.get("currservice_end", "")) + def update_status(self, evn): + if evn: + s_duration = int(evn.get("e2eventstart", "0")) + s_time = datetime.fromtimestamp(s_duration) + end_time = datetime.fromtimestamp(s_duration + int(evn.get("e2eventduration", "0"))) + dsc = "{} {}:{} - {}:{}".format(evn.get("e2eventtitle", ""), + s_time.hour, s_time.minute, + end_time.hour, end_time.minute) self._service_epg_label.set_text(dsc) - self._service_epg_label.set_tooltip_text(status.get("currservice_description", "")) + self._service_epg_label.set_tooltip_text(evn.get("e2eventdescription", "")) # ***************** Filter and search *********************# @@ -2108,11 +2112,6 @@ class Application(Gtk.Application): def get_format_version(self): return 5 if self._settings.v5_support else 4 - @run_idle - def update_info_boxes_visible(self, visible): - self._signal_box.set_visible(visible) - self._receiver_info_box.set_visible(visible) - @run_idle def show_error_dialog(self, message): show_dialog(DialogType.ERROR, self._main_window, message) diff --git a/app/ui/transmitter.py b/app/ui/transmitter.py index e286edb6..f436f39b 100644 --- a/app/ui/transmitter.py +++ b/app/ui/transmitter.py @@ -88,7 +88,7 @@ class LinksTransmitter: def on_play(self, res): """ Play callback """ GLib.idle_add(self._url_entry.set_sensitive, True) - res = res.get("result", None) if res else res + res = res.get("e2state", None) if res else res self._url_entry.set_name("GtkEntry" if res else "digit-entry") def on_exit(self, item=None):