From 77bb4a7fef642ff993484b2e1ada276e4b47d361 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Mon, 16 Dec 2019 15:45:41 +0300 Subject: [PATCH 01/21] minor player fix --- app/tools/media.py | 1 + app/ui/main_app_window.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/tools/media.py b/app/tools/media.py index 2df7c3ea..3479b29c 100644 --- a/app/tools/media.py +++ b/app/tools/media.py @@ -11,6 +11,7 @@ class Player: from app.tools.vlc import EventType except OSError as e: log("{}: Load library error: {}".format(__class__.__name__, e)) + raise ImportError else: self._is_playing = False args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib") diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index 0a9ed14e..79b7c91e 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -1556,7 +1556,7 @@ class Application(Gtk.Application): try: self._player = Player.get_instance(rewind_callback=self.on_player_duration_changed, position_callback=self.on_player_time_changed) - except (NameError, AttributeError): + except (ImportError, NameError, AttributeError): self.show_error_dialog("No VLC is found. Check that it is installed!") return else: @@ -1618,7 +1618,6 @@ class Application(Gtk.Application): self._player.set_nso(widget) else: self._drawing_area_xid = widget.get_window().get_xid() - print(self._drawing_area_xid) self._player.set_xwindow(self._drawing_area_xid) def on_player_drawing_area_draw(self, widget, cr): From 3859c84c0ebb97482069fca73bb4b53a105b1690 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Tue, 17 Dec 2019 11:59:57 +0300 Subject: [PATCH 02/21] added exception --- app/connections.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/connections.py b/app/connections.py index 7f5d3df9..bdce6cac 100644 --- a/app/connections.py +++ b/app/connections.py @@ -5,6 +5,7 @@ import time import urllib 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 @@ -329,7 +330,7 @@ def test_http(host, port, user, password, timeout=5, skip_message=False): with urlopen(url, timeout=5) as f: return json.loads(f.read().decode("utf-8")).get("message", "") - except (URLError, HTTPError) as e: + except (RemoteDisconnected, URLError, HTTPError) as e: raise TestException(e) From 5aec42548e1bdeb65e6572c4a4dc0088ed20d341 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Sun, 22 Dec 2019 20:42:29 +0300 Subject: [PATCH 03/21] settings refactoring --- app/connections.py | 18 +- app/eparser/__init__.py | 26 +- app/eparser/iptv.py | 20 +- app/settings.py | 305 +++--- app/tools/picons.py | 16 +- app/ui/backup.py | 10 +- app/ui/dialogs.py | 2 +- app/ui/download_dialog.py | 14 +- app/ui/epg_dialog.py | 2 +- app/ui/imports.py | 16 +- app/ui/iptv.py | 16 +- app/ui/main_app_window.py | 129 +-- app/ui/main_helper.py | 28 +- app/ui/main_window.glade | 102 +- app/ui/picons_downloader.py | 14 +- app/ui/satellites_dialog.py | 2 +- app/ui/service_details_dialog.py | 32 +- app/ui/settings_dialog.glade | 1693 +++++++++++++++++------------- app/ui/settings_dialog.py | 105 +- app/ui/uicommons.py | 1 + 20 files changed, 1414 insertions(+), 1137 deletions(-) diff --git a/app/connections.py b/app/connections.py index bdce6cac..ea9cff51 100644 --- a/app/connections.py +++ b/app/connections.py @@ -12,7 +12,7 @@ from urllib.parse import urlencode from urllib.request import urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener, install_opener from app.commons import log -from app.settings import Profile +from app.settings import SettingsType _BQ_FILES_LIST = ("tv", "radio", # enigma 2 "myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino @@ -49,7 +49,7 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=print): with FTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp: ftp.encoding = "utf-8" callback("FTP OK.\n") - save_path = settings.data_dir_path + save_path = settings.data_local_path os.makedirs(os.path.dirname(save_path), exist_ok=True) files = [] # bouquets @@ -94,14 +94,14 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=print): def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False, callback=print, done_callback=None, use_http=False): - profile = settings.profile - data_path = settings.data_dir_path + s_type = settings.setting_type + data_path = settings.data_local_path host = settings.host base_url = "http://{}:{}/api/".format(host, settings.http_port) tn, ht = None, None # telnet, http try: - if profile is Profile.ENIGMA_2 and use_http: + if s_type is SettingsType.ENIGMA_2 and use_http: ht = http(settings.http_user, settings.http_password, base_url, callback) next(ht) message = "" @@ -139,7 +139,7 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False if download_type is DownloadType.SATELLITES: upload_xml(ftp, data_path, sat_xml_path, _SAT_XML_FILE, callback) - if profile is Profile.NEUTRINO_MP and download_type is DownloadType.WEBTV: + if s_type is SettingsType.NEUTRINO_MP and download_type is DownloadType.WEBTV: upload_xml(ftp, data_path, sat_xml_path, _WEBTV_XML_FILE, callback) if download_type is DownloadType.BOUQUETS: @@ -148,7 +148,7 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False if download_type is DownloadType.ALL: upload_xml(ftp, data_path, sat_xml_path, _SAT_XML_FILE, callback) - if profile is Profile.NEUTRINO_MP: + if s_type is SettingsType.NEUTRINO_MP: upload_xml(ftp, data_path, sat_xml_path, _WEBTV_XML_FILE, callback) ftp.cwd(services_path) @@ -156,11 +156,11 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False upload_files(ftp, data_path, _DATA_FILES_LIST, callback) if download_type is DownloadType.PICONS: - upload_picons(ftp, settings.picons_dir_path, settings.picons_path, callback) + upload_picons(ftp, settings.picons_local_path, settings.picons_path, callback) if tn and not use_http: # resume enigma or restart neutrino - tn.send("init 3" if profile is Profile.ENIGMA_2 else "init 6") + 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") diff --git a/app/eparser/__init__.py b/app/eparser/__init__.py index fff031a5..d514d26b 100644 --- a/app/eparser/__init__.py +++ b/app/eparser/__init__.py @@ -1,5 +1,5 @@ from app.commons import run_task -from app.settings import Profile +from app.settings import SettingsType from .ecommons import Service, Satellite, Transponder, Bouquet, Bouquets, is_transponder_valid from .enigma.blacklist import get_blacklist, write_blacklist from .enigma.bouquets import get_bouquets as get_enigma_bouquets, write_bouquets as write_enigma_bouquets, to_bouquet_id @@ -10,33 +10,33 @@ from .neutrino.services import get_services as get_neutrino_services, write_serv from .satxml import get_satellites, write_satellites -def get_services(data_path, profile, format_version): - if profile is Profile.ENIGMA_2: +def get_services(data_path, s_type, format_version): + if s_type is SettingsType.ENIGMA_2: return get_enigma_services(data_path, format_version) - elif profile is Profile.NEUTRINO_MP: + elif s_type is SettingsType.NEUTRINO_MP: return get_neutrino_services(data_path) @run_task -def write_services(path, channels, profile, format_version): - if profile is Profile.ENIGMA_2: +def write_services(path, channels, s_type, format_version): + if s_type is SettingsType.ENIGMA_2: write_enigma_services(path, channels, format_version) - elif profile is Profile.NEUTRINO_MP: + elif s_type is SettingsType.NEUTRINO_MP: write_neutrino_services(path, channels) -def get_bouquets(path, profile): - if profile is Profile.ENIGMA_2: +def get_bouquets(path, s_type): + if s_type is SettingsType.ENIGMA_2: return get_enigma_bouquets(path) - elif profile is Profile.NEUTRINO_MP: + elif s_type is SettingsType.NEUTRINO_MP: return get_neutrino_bouquets(path) @run_task -def write_bouquets(path, bouquets, profile): - if profile is Profile.ENIGMA_2: +def write_bouquets(path, bouquets, s_type): + if s_type is SettingsType.ENIGMA_2: write_enigma_bouquets(path, bouquets) - elif profile is Profile.NEUTRINO_MP: + elif s_type is SettingsType.NEUTRINO_MP: write_neutrino_bouquets(path, bouquets) diff --git a/app/eparser/iptv.py b/app/eparser/iptv.py index b47a8967..55451b5e 100644 --- a/app/eparser/iptv.py +++ b/app/eparser/iptv.py @@ -3,7 +3,7 @@ import re import urllib.request from enum import Enum -from app.settings import Profile +from app.settings import SettingsType from app.ui.uicommons import IPTV_ICON from .ecommons import BqServiceType, Service @@ -20,18 +20,18 @@ class StreamType(Enum): NONE_REC_2 = "5002" -def parse_m3u(path, profile): +def parse_m3u(path, s_type): with open(path) as file: aggr = [None] * 10 services = [] groups = set() counter = 0 name = None - + for line in file.readlines(): if line.startswith("#EXTINF"): name = line[1 + line.index(","):].strip() - elif line.startswith("#EXTGRP") and profile is Profile.ENIGMA_2: + elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2: grp_name = line.strip("#EXTGRP:").strip() if grp_name not in groups: groups.add(grp_name) @@ -41,7 +41,7 @@ def parse_m3u(path, profile): services.append(mr) elif not line.startswith("#"): url = line.strip() - fav_id = get_fav_id(url, name, profile) + fav_id = get_fav_id(url, name, s_type) if name and url: srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], BqServiceType.IPTV.name, *aggr, fav_id, None) services.append(srv) @@ -49,8 +49,8 @@ def parse_m3u(path, profile): return services -def export_to_m3u(path, bouquet, profile): - pattern = re.compile(".*:(http.*):.*") if profile is Profile.ENIGMA_2 else re.compile("(http.*?)::::.*") +def export_to_m3u(path, bouquet, s_type): + pattern = re.compile(".*:(http.*):.*") if s_type is SettingsType.ENIGMA_2 else re.compile("(http.*?)::::.*") lines = ["#EXTM3U\n"] current_grp = None @@ -72,13 +72,13 @@ def export_to_m3u(path, bouquet, profile): file.writelines(lines) -def get_fav_id(url, service_name, profile): +def get_fav_id(url, service_name, s_type): """ Returns fav id depending on the profile. """ - if profile is Profile.ENIGMA_2: + if s_type is SettingsType.ENIGMA_2: url = urllib.request.quote(url) stream_type = StreamType.NONE_TS.value return ENIGMA2_FAV_ID_FORMAT.format(stream_type, 1, 0, 0, 0, 0, url, service_name, service_name, None) - elif profile is Profile.NEUTRINO_MP: + elif s_type is SettingsType.NEUTRINO_MP: return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1) diff --git a/app/settings.py b/app/settings.py index a73cd704..9fa3633a 100644 --- a/app/settings.py +++ b/app/settings.py @@ -2,7 +2,7 @@ import json import os from pprint import pformat from textwrap import dedent -from enum import Enum +from enum import Enum, IntEnum from pathlib import Path CONFIG_PATH = str(Path.home()) + "/.config/demon-editor/" @@ -10,19 +10,85 @@ CONFIG_FILE = CONFIG_PATH + "config.json" DATA_PATH = "data/" -class Profile(Enum): +class Defaults(Enum): + """ Default program settings """ + DEFAULT_PROFILE = "default" + BACKUP_BEFORE_DOWNLOADING = True + BACKUP_BEFORE_SAVE = True + V5_SUPPORT = False + HTTP_API_SUPPORT = False + ENABLE_YT_DL = False + ENABLE_SEND_TO = False + USE_COLORS = True + NEW_COLOR = "rgb(255,230,204)" + EXTRA_COLOR = "rgb(179,230,204)" + FAV_CLICK_MODE = 0 + + +def get_default_settings(): + return { + "version": 1, + "default_profile": Defaults.DEFAULT_PROFILE.value, + "profiles": {"default": SettingsType.ENIGMA_2.get_default_settings()}, + "v5_support": Defaults.V5_SUPPORT.value, + "http_api_support": Defaults.HTTP_API_SUPPORT.value, + "enable_yt_dl": Defaults.ENABLE_YT_DL.value, + "enable_send_to": Defaults.ENABLE_SEND_TO.value, + "use_colors": Defaults.USE_COLORS.value, + "new_color": Defaults.NEW_COLOR.value, + "extra_color": Defaults.EXTRA_COLOR.value, + "fav_click_mode": Defaults.FAV_CLICK_MODE.value + } + + +class SettingsType(IntEnum): """ Profiles for settings """ - ENIGMA_2 = "0" - NEUTRINO_MP = "1" + ENIGMA_2 = 0 + NEUTRINO_MP = 1 + + def get_default_settings(self): + """ Returns default settings for current type """ + if self is self.ENIGMA_2: + return {"setting_type": self, + "host": "127.0.0.1", "port": "21", "user": "root", "password": "root", "timeout": 5, + "http_user": "root", "http_password": "", "http_port": "80", "http_timeout": 5, + "telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 5, + "services_path": "/etc/enigma2/", "user_bouquet_path": "/etc/enigma2/", + "satellites_xml_path": "/etc/tuxbox/", "data_local_path": DATA_PATH + "enigma2/", + "picons_path": "/usr/share/enigma2/picon", + "picons_local_path": DATA_PATH + "enigma2/picons/", + "backup_local_path": DATA_PATH + "enigma2/backup/"} + elif self is self.NEUTRINO_MP: + return {"setting_type": self, + "host": "127.0.0.1", "port": "21", "user": "root", "password": "root", "timeout": 5, + "http_user": "", "http_password": "", "http_port": "80", "http_timeout": 2, + "telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 1, + "services_path": "/var/tuxbox/config/zapit/", "user_bouquet_path": "/var/tuxbox/config/zapit/", + "satellites_xml_path": "/var/tuxbox/config/", "data_local_path": DATA_PATH + "neutrino/", + "picons_path": "/usr/share/tuxbox/neutrino/icons/logo/", + "picons_local_path": DATA_PATH + "neutrino/picons/", + "backup_local_path": DATA_PATH + "neutrino/backup/"} + + +class SettingsException(Exception): + pass class Settings: __INSTANCE = None + __VERSION = 1 def __init__(self): - self._config = get_config() - self._current_profile = Profile(self._config.get("profile")) - self._current_profile_options = self._config.get(self._current_profile.value) + settings = get_settings() + + if self.__VERSION > settings.get("version", 0): + write_settings(get_default_settings()) + raise SettingsException("Outdated version of the settings format!") + + self._settings = settings + self._current_profile = self._settings.get("default_profile", "default") + self._profiles = self._settings.get("profiles", {"default": SettingsType.ENIGMA_2.get_default_settings()}) + self._cp_settings = self._profiles.get(self._current_profile) # Current profile settings def __str__(self): return dedent(""" Current profile: {} @@ -31,8 +97,8 @@ class Settings: Full config: {} """).format(self._current_profile, - pformat(self._current_profile_options), - pformat(self._config)) + pformat(self._cp_settings), + pformat(self._settings)) @classmethod def get_instance(cls): @@ -41,325 +107,296 @@ class Settings: return cls.__INSTANCE def save(self): - write_config(self._config) + write_settings(self._settings) def reset(self, force_write=False): - def_settings = get_default_settings() - for p in Profile: - current = self._config.get(p.value) - default = def_settings.get(p.value) - for k in default: - current[k] = default.get(k) + for k, v in self.setting_type.get_default_settings().items(): + self._cp_settings[k] = v if force_write: - write_config(get_default_settings()) + self.save() + + def get_default(self, p_name): + """ Returns default value for current settings type """ + return self.setting_type.get_default_settings().get(p_name) def add(self, name, value): """ Adds extra options """ - self._config[name] = value + self._settings[name] = value def get(self, name): - """ Returns extra options """ - return self._config.get(name, None) - - def get_default(self, name): - """ Returns default value of the option """ - return get_default_settings().get(self._current_profile.value).get(name) + """ Returns extra options or None """ + return self._settings.get(name, None) @property - def presets(self): - raise NotImplementedError + def profiles(self): + return self._profiles - @presets.setter - def presets(self, name): - raise NotImplementedError + @profiles.setter + def profiles(self, ps): + self._profiles = ps + self._settings["profiles"] = self._profiles @property - def profile(self): - return self._current_profile + def setting_type(self): + return SettingsType(self._cp_settings.get("setting_type", SettingsType.ENIGMA_2.value)) - @profile.setter - def profile(self, prf): - self._current_profile = prf - self._config["profile"] = prf.value - self._current_profile_options = self._config.get(prf.value) + @setting_type.setter + def setting_type(self, s_type): + self._cp_settings["setting_type"] = s_type + for k, v in s_type.get_default_settings().items(): + self._cp_settings[k] = v @property def host(self): - return self._current_profile_options.get("host", self.get_default("host")) + return self._cp_settings.get("host", self.get_default("host")) @host.setter def host(self, value): - self._current_profile_options["host"] = value + self._cp_settings["host"] = value @property def port(self): - return self._current_profile_options.get("port", self.get_default("port")) + return self._cp_settings.get("port", self.get_default("port")) @port.setter def port(self, value): - self._current_profile_options["port"] = value + self._cp_settings["port"] = value @property def user(self): - return self._current_profile_options.get("user", self.get_default("user")) + return self._cp_settings.get("user", self.get_default("user")) @user.setter def user(self, value): - self._current_profile_options["user"] = value + self._cp_settings["user"] = value @property def password(self): - return self._current_profile_options.get("password", self.get_default("password")) + return self._cp_settings.get("password", self.get_default("password")) @password.setter def password(self, value): - self._current_profile_options["password"] = value + self._cp_settings["password"] = value @property def http_user(self): - return self._current_profile_options.get("http_user", self.get_default("http_user")) + return self._cp_settings.get("http_user", self.get_default("http_user")) @http_user.setter def http_user(self, value): - self._current_profile_options["http_user"] = value + self._cp_settings["http_user"] = value @property def http_password(self): - return self._current_profile_options.get("http_password", self.get_default("http_password")) + return self._cp_settings.get("http_password", self.get_default("http_password")) @http_password.setter def http_password(self, value): - self._current_profile_options["http_password"] = value + self._cp_settings["http_password"] = value @property def http_port(self): - return self._current_profile_options.get("http_port", self.get_default("http_port")) + return self._cp_settings.get("http_port", self.get_default("http_port")) @http_port.setter def http_port(self, value): - self._current_profile_options["http_port"] = value + self._cp_settings["http_port"] = value @property def http_timeout(self): - return self._current_profile_options.get("http_timeout", self.get_default("http_timeout")) + return self._cp_settings.get("http_timeout", self.get_default("http_timeout")) @http_timeout.setter def http_timeout(self, value): - self._current_profile_options["http_timeout"] = value + self._cp_settings["http_timeout"] = value @property def telnet_user(self): - return self._current_profile_options.get("telnet_user", self.get_default("telnet_user")) + return self._cp_settings.get("telnet_user", self.get_default("telnet_user")) @telnet_user.setter def telnet_user(self, value): - self._current_profile_options["telnet_user"] = value + self._cp_settings["telnet_user"] = value @property def telnet_password(self): - return self._current_profile_options.get("telnet_password", self.get_default("telnet_password")) + return self._cp_settings.get("telnet_password", self.get_default("telnet_password")) @telnet_password.setter def telnet_password(self, value): - self._current_profile_options["telnet_password"] = value + self._cp_settings["telnet_password"] = value @property def telnet_port(self): - return self._current_profile_options.get("telnet_port", self.get_default("telnet_port")) + return self._cp_settings.get("telnet_port", self.get_default("telnet_port")) @telnet_port.setter def telnet_port(self, value): - self._current_profile_options["telnet_port"] = value + self._cp_settings["telnet_port"] = value @property def telnet_timeout(self): - return self._current_profile_options.get("telnet_timeout", self.get_default("telnet_timeout")) + return self._cp_settings.get("telnet_timeout", self.get_default("telnet_timeout")) @telnet_timeout.setter def telnet_timeout(self, value): - self._current_profile_options["telnet_timeout"] = value + self._cp_settings["telnet_timeout"] = value @property def services_path(self): - return self._current_profile_options.get("services_path", self.get_default("services_path")) + return self._cp_settings.get("services_path", self.get_default("services_path")) @services_path.setter def services_path(self, value): - self._current_profile_options["services_path"] = value + self._cp_settings["services_path"] = value @property def user_bouquet_path(self): - return self._current_profile_options.get("user_bouquet_path", self.get_default("user_bouquet_path")) + return self._cp_settings.get("user_bouquet_path", self.get_default("user_bouquet_path")) @user_bouquet_path.setter def user_bouquet_path(self, value): - self._current_profile_options["user_bouquet_path"] = value + self._cp_settings["user_bouquet_path"] = value @property def satellites_xml_path(self): - return self._current_profile_options.get("satellites_xml_path", self.get_default("satellites_xml_path")) + return self._cp_settings.get("satellites_xml_path", self.get_default("satellites_xml_path")) @satellites_xml_path.setter def satellites_xml_path(self, value): - self._current_profile_options["satellites_xml_path"] = value + self._cp_settings["satellites_xml_path"] = value @property - def data_dir_path(self): - return self._current_profile_options.get("data_dir_path", self.get_default("data_dir_path")) + def data_local_path(self): + return self._cp_settings.get("data_local_path", self.get_default("data_local_path")) - @data_dir_path.setter - def data_dir_path(self, value): - self._current_profile_options["data_dir_path"] = value + @data_local_path.setter + def data_local_path(self, value): + self._cp_settings["data_local_path"] = value @property def picons_path(self): - return self._current_profile_options.get("picons_path", self.get_default("picons_path")) + return self._cp_settings.get("picons_path", self.get_default("picons_path")) @picons_path.setter def picons_path(self, value): - self._current_profile_options["picons_path"] = value + self._cp_settings["picons_path"] = value @property - def picons_dir_path(self): - return self._current_profile_options.get("picons_dir_path", self.get_default("picons_dir_path")) + def picons_local_path(self): + return self._cp_settings.get("picons_local_path", self.get_default("picons_local_path")) - @picons_dir_path.setter - def picons_dir_path(self, value): - self._current_profile_options["picons_dir_path"] = value + @picons_local_path.setter + def picons_local_path(self, value): + self._cp_settings["picons_local_path"] = value @property - def backup_dir_path(self): - return self._current_profile_options.get("backup_dir_path", self.get_default("backup_dir_path")) + def backup_local_path(self): + return self._cp_settings.get("backup_local_path", self.get_default("backup_local_path")) - @backup_dir_path.setter - def backup_dir_path(self, value): - self._current_profile_options["backup_dir_path"] = value + @backup_local_path.setter + def backup_local_path(self, value): + self._cp_settings["backup_local_path"] = value + + # ***** Program settings ***** @property def backup_before_save(self): - return self._current_profile_options.get("backup_before_save", self.get_default("backup_before_save")) + return self._settings.get("backup_before_save", Defaults.BACKUP_BEFORE_SAVE.value) @backup_before_save.setter def backup_before_save(self, value): - self._current_profile_options["backup_before_save"] = value + self._settings["backup_before_save"] = value @property def backup_before_downloading(self): - return self._current_profile_options.get("backup_before_downloading", - self.get_default("backup_before_downloading")) + return self._settings.get("backup_before_downloading", Defaults.BACKUP_BEFORE_DOWNLOADING.value) @backup_before_downloading.setter def backup_before_downloading(self, value): - self._current_profile_options["backup_before_downloading"] = value + self._settings["backup_before_downloading"] = value @property def v5_support(self): - return self._current_profile_options.get("v5_support", self.get_default("v5_support")) + return self._settings.get("v5_support", Defaults.V5_SUPPORT.value) @v5_support.setter def v5_support(self, value): - self._current_profile_options["v5_support"] = value + self._settings["v5_support"] = value @property def http_api_support(self): - return self._current_profile_options.get("http_api_support", self.get_default("http_api_support")) + return self._settings.get("http_api_support", Defaults.HTTP_API_SUPPORT.value) @http_api_support.setter def http_api_support(self, value): - self._current_profile_options["http_api_support"] = value + self._settings["http_api_support"] = value @property def enable_yt_dl(self): - return self._current_profile_options.get("enable_yt_dl", self.get_default("enable_yt_dl")) + return self._settings.get("enable_yt_dl", Defaults.ENABLE_YT_DL.value) @enable_yt_dl.setter def enable_yt_dl(self, value): - self._current_profile_options["enable_yt_dl"] = value + self._settings["enable_yt_dl"] = value @property def enable_send_to(self): - return self._current_profile_options.get("enable_send_to", self.get_default("enable_send_to")) + return self._settings.get("enable_send_to", Defaults.ENABLE_SEND_TO.value) @enable_send_to.setter def enable_send_to(self, value): - self._current_profile_options["enable_send_to"] = value + self._settings["enable_send_to"] = value @property def use_colors(self): - return self._current_profile_options.get("use_colors", self.get_default("use_colors")) + return self._settings.get("use_colors", Defaults.USE_COLORS.value) @use_colors.setter def use_colors(self, value): - self._current_profile_options["use_colors"] = value + self._settings["use_colors"] = value @property def new_color(self): - return self._current_profile_options.get("new_color", self.get_default("new_color")) + return self._settings.get("new_color", Defaults.NEW_COLOR.value) @new_color.setter def new_color(self, value): - self._current_profile_options["new_color"] = value + self._settings["new_color"] = value @property def extra_color(self): - return self._current_profile_options.get("extra_color", self.get_default("extra_color")) + return self._settings.get("extra_color", Defaults.EXTRA_COLOR.value) @extra_color.setter def extra_color(self, value): - self._current_profile_options["extra_color"] = value + self._settings["extra_color"] = value @property def fav_click_mode(self): - return self._current_profile_options.get("fav_click_mode", self.get_default("fav_click_mode")) + return self._settings.get("fav_click_mode", Defaults.FAV_CLICK_MODE.value) @fav_click_mode.setter def fav_click_mode(self, value): - self._current_profile_options["fav_click_mode"] = value + self._settings["fav_click_mode"] = value -def get_config(): +def get_settings(): os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True) # create dir if not exist os.makedirs(os.path.dirname(DATA_PATH), exist_ok=True) if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0: - write_config(get_default_settings()) + write_settings(get_default_settings()) with open(CONFIG_FILE, "r") as config_file: return json.load(config_file) -def write_config(config): +def write_settings(config): with open(CONFIG_FILE, "w") as config_file: json.dump(config, config_file, indent=" ") -def get_default_settings(): - return { - Profile.ENIGMA_2.value: { - "host": "127.0.0.1", "port": "21", "user": "root", "password": "root", - "http_user": "root", "http_password": "", "http_port": "80", "http_timeout": 5, - "telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 5, - "services_path": "/etc/enigma2/", "user_bouquet_path": "/etc/enigma2/", - "satellites_xml_path": "/etc/tuxbox/", "data_dir_path": DATA_PATH + "enigma2/", - "picons_path": "/usr/share/enigma2/picon", "picons_dir_path": DATA_PATH + "enigma2/picons/", - "backup_dir_path": DATA_PATH + "enigma2/backup/", - "backup_before_save": True, "backup_before_downloading": True, - "v5_support": False, "http_api_support": False, "enable_yt_dl": False, "enable_send_to": False, - "use_colors": True, "new_color": "rgb(255,230,204)", "extra_color": "rgb(179,230,204)", - "fav_click_mode": 0}, - Profile.NEUTRINO_MP.value: { - "host": "127.0.0.1", "port": "21", "user": "root", "password": "root", - "http_user": "", "http_password": "", "http_port": "80", "http_timeout": 2, - "telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 1, - "services_path": "/var/tuxbox/config/zapit/", "user_bouquet_path": "/var/tuxbox/config/zapit/", - "satellites_xml_path": "/var/tuxbox/config/", "data_dir_path": DATA_PATH + "neutrino/", - "picons_path": "/usr/share/tuxbox/neutrino/icons/logo/", "picons_dir_path": DATA_PATH + "neutrino/picons/", - "backup_dir_path": DATA_PATH + "neutrino/backup/", - "backup_before_save": True, "backup_before_downloading": True, - "fav_click_mode": 0}, - "profile": Profile.ENIGMA_2.value} - - if __name__ == "__main__": pass diff --git a/app/tools/picons.py b/app/tools/picons.py index 87f3d5a4..9e4419bf 100644 --- a/app/tools/picons.py +++ b/app/tools/picons.py @@ -7,7 +7,7 @@ from collections import namedtuple from html.parser import HTMLParser from app.commons import run_task -from app.settings import Profile +from app.settings import SettingsType _ENIGMA2_PICON_KEY = "{:X}:{:X}:{}" _NEUTRINO_PICON_KEY = "{:x}{:04x}{:04x}.png" @@ -79,7 +79,7 @@ class PiconsParser(HTMLParser): pass @staticmethod - def parse(open_path, picons_path, tmp_path, provider, picon_ids, profile=Profile.ENIGMA_2): + def parse(open_path, picons_path, tmp_path, provider, picon_ids, s_type=SettingsType.ENIGMA_2): with open(open_path, encoding="utf-8", errors="replace") as f: on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single neg_pos = pos.endswith("W") @@ -100,7 +100,7 @@ class PiconsParser(HTMLParser): namespace = "{:X}{:X}".format(int(pos), int(freq)) else: namespace = "{:X}0000".format(int(pos)) - name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, profile) + name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, s_type) p_name = picons_path + (name if name else os.path.basename(p.ref)) shutil.copyfile(tmp_path + "www.lyngsat.com/" + p.ref.lstrip("."), p_name) except (TypeError, ValueError) as e: @@ -109,10 +109,10 @@ class PiconsParser(HTMLParser): print(msg) @staticmethod - def format(ssid, on_id, namespace, picon_ids, profile: Profile): - if profile is Profile.ENIGMA_2: + def format(ssid, on_id, namespace, picon_ids, s_type): + if s_type is SettingsType.ENIGMA_2: return picon_ids.get(_ENIGMA2_PICON_KEY.format(int(ssid), int(on_id), namespace), None) - elif profile is Profile.NEUTRINO_MP: + elif s_type is SettingsType.NEUTRINO_MP: tr_id = int(ssid[:-2] if len(ssid) < 4 else ssid[:2]) return _NEUTRINO_PICON_KEY.format(tr_id, int(on_id), int(ssid)) else: @@ -249,12 +249,12 @@ def parse_providers(open_path): @run_task -def convert_to(src_path, dest_path, profile, callback, done_callback): +def convert_to(src_path, dest_path, s_type, callback, done_callback): """ Converts names format of picons. Copies resulting files from src to dest and writes state to callback. """ - pattern = "/*_0_0_0.png" if profile is Profile.ENIGMA_2 else "/*.png" + pattern = "/*_0_0_0.png" if s_type is SettingsType.ENIGMA_2 else "/*.png" for file in glob.glob(src_path + pattern): base_name = os.path.basename(file) pic_data = base_name.rstrip(".png").split("_") diff --git a/app/ui/backup.py b/app/ui/backup.py index 7aca7613..048ff058 100644 --- a/app/ui/backup.py +++ b/app/ui/backup.py @@ -7,7 +7,7 @@ from datetime import datetime from enum import Enum from app.commons import run_idle -from app.settings import Profile +from app.settings import SettingsType from app.ui.dialogs import show_dialog, DialogType from app.ui.main_helper import append_text_to_tview from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey @@ -36,9 +36,9 @@ class BackupDialog: builder.connect_signals(handlers) self._settings = settings - self._profile = settings.profile - self._data_path = self._settings.data_dir_path - self._backup_path = self._settings.backup_dir_path or self._data_path + "backup/" + self._s_type = settings.setting_type + self._data_path = self._settings.data_local_path + self._backup_path = self._settings.backup_local_path or self._data_path + "backup/" self._open_data_callback = callback self._dialog_window = builder.get_object("dialog_window") self._dialog_window.set_transient_for(transient) @@ -152,7 +152,7 @@ class BackupDialog: shutil.unpack_archive(full_file_name, self._data_path) elif restore_type is RestoreType.BOUQUETS: tmp_dir = tempfile.gettempdir() + "/" + file_name - cond = (".tv", ".radio") if self._profile is Profile.ENIGMA_2 else "bouquets.xml" + cond = (".tv", ".radio") if self._s_type is SettingsType.ENIGMA_2 else "bouquets.xml" shutil.unpack_archive(full_file_name, tmp_dir) for file in filter(lambda f: f.endswith(cond), os.listdir(self._data_path)): os.remove(os.path.join(self._data_path, file)) diff --git a/app/ui/dialogs.py b/app/ui/dialogs.py index 596b89f6..8601ca8a 100644 --- a/app/ui/dialogs.py +++ b/app/ui/dialogs.py @@ -101,7 +101,7 @@ def get_file_chooser_dialog(transient, text, settings, action_type, file_filter) if file_filter is not None: dialog.add_filter(file_filter) - path = settings.data_dir_path + path = settings.data_local_path dialog.set_current_folder(path) response = dialog.run() if response == Gtk.ResponseType.OK: diff --git a/app/ui/download_dialog.py b/app/ui/download_dialog.py index 91d0d9c5..76fb06d6 100644 --- a/app/ui/download_dialog.py +++ b/app/ui/download_dialog.py @@ -2,7 +2,7 @@ from gi.repository import GLib from app.commons import run_idle, run_task from app.connections import download_data, DownloadType, upload_data -from app.settings import Profile +from app.settings import SettingsType from app.ui.backup import backup_data, restore_data from app.ui.main_helper import append_text_to_tview from app.ui.settings_dialog import show_settings_dialog @@ -12,7 +12,7 @@ from .dialogs import show_dialog, DialogType, get_message class DownloadDialog: def __init__(self, transient, settings, open_data_callback, update_settings_callback): - self._profile = settings.profile + self._s_type = settings.setting_type self._settings = settings self._open_data_callback = open_data_callback self._update_settings_callback = update_settings_callback @@ -59,8 +59,8 @@ class DownloadDialog: def init_settings(self): self._host_entry.set_text(self._settings.host) - self._data_path_entry.set_text(self._settings.data_dir_path) - is_enigma = self._profile is Profile.ENIGMA_2 + self._data_path_entry.set_text(self._settings.data_local_path) + is_enigma = self._s_type is SettingsType.ENIGMA_2 self._webtv_radio_button.set_visible(not is_enigma) self._http_radio_button.set_visible(is_enigma) self._use_http_box.set_visible(is_enigma) @@ -111,7 +111,7 @@ class DownloadDialog: def on_preferences(self, item): response = show_settings_dialog(self._dialog_window, self._settings) if response != Gtk.ResponseType.CANCEL: - self._profile = self._settings.profile + self._s_type = self._settings.setting_type self.init_settings() gen = self._update_settings_callback() GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) @@ -134,8 +134,8 @@ class DownloadDialog: try: if download: if backup and d_type is not DownloadType.SATELLITES: - data_path = self._settings.data_dir_path or self._data_path_entry.get_text() - backup_path = self._settings.backup_dir_path or data_path + "backup/" + data_path = self._settings.data_local_path or self._data_path_entry.get_text() + backup_path = self._settings.backup_local_path or data_path + "backup/" backup_src = backup_data(data_path, backup_path, d_type is DownloadType.ALL) download_data(settings=self._settings, download_type=d_type, callback=self.append_output) else: diff --git a/app/ui/epg_dialog.py b/app/ui/epg_dialog.py index de6e49c3..151bea9f 100644 --- a/app/ui/epg_dialog.py +++ b/app/ui/epg_dialog.py @@ -483,7 +483,7 @@ class EpgDialog: # ***************** Options *********************# def init_options(self): - epg_dat_path = self._settings.data_dir_path + "epg/" + epg_dat_path = self._settings.data_local_path + "epg/" self._epg_dat_path_entry.set_text(epg_dat_path) default_epg_data_stb_path = "/etc/enigma2" epg_options = self._settings.get("epg_options") diff --git a/app/ui/imports.py b/app/ui/imports.py index bcbf979c..8c203759 100644 --- a/app/ui/imports.py +++ b/app/ui/imports.py @@ -6,7 +6,7 @@ from app.eparser import get_bouquets, get_services from app.eparser.ecommons import BqType, BqServiceType, Bouquet from app.eparser.enigma.bouquets import get_bouquet from app.eparser.neutrino.bouquets import parse_webtv, parse_bouquets as get_neutrino_bouquets -from app.settings import Profile +from app.settings import SettingsType from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message from app.ui.main_helper import on_popup_menu from .uicommons import Gtk, UI_RESOURCES_PATH, KeyboardKey, Column @@ -17,12 +17,12 @@ def import_bouquet(transient, model, path, settings, services, appender): itr = model.get_iter(path) bq_type = BqType(model.get(itr, Column.BQ_TYPE)[0]) pattern, f_pattern = None, None - profile = settings.profile + profile = settings.setting_type - if profile is Profile.ENIGMA_2: + if profile is SettingsType.ENIGMA_2: pattern = ".{}".format(bq_type.value) f_pattern = "userbouquet.*{}".format(pattern) - elif profile is Profile.NEUTRINO_MP: + elif profile is SettingsType.NEUTRINO_MP: pattern = "webtv.xml" if bq_type is BqType.WEBTV else "bouquets.xml" f_pattern = "bouquets.xml" if bq_type is BqType.TV: @@ -38,7 +38,7 @@ def import_bouquet(transient, model, path, settings, services, appender): show_dialog(DialogType.ERROR, transient, text="No bouquet file is selected!") return - if profile is Profile.ENIGMA_2: + if profile is SettingsType.ENIGMA_2: bq = get_enigma2_bouquet(file_path) imported = list(filter(lambda x: x.data in services or x.type is BqServiceType.IPTV, bq.services)) @@ -51,7 +51,7 @@ def import_bouquet(transient, model, path, settings, services, appender): else: p_itr = model.iter_parent(itr) appender(bq, p_itr) if p_itr else appender(bq, itr) - elif profile is Profile.NEUTRINO_MP: + elif profile is SettingsType.NEUTRINO_MP: if bq_type is BqType.WEBTV: bqs = parse_webtv(file_path, "WEBTV", bq_type.value) else: @@ -90,7 +90,7 @@ class ImportDialog: self._services = {} self._service_ids = service_ids self._append = appender - self._profile = settings.profile + self._profile = settings.setting_type self._settings = settings self._bouquets = bouquets @@ -125,7 +125,7 @@ class ImportDialog: self._main_model.append((bq.name, bq.type, True)) self._bq_services[(bq.name, bq.type)] = bq.services # Note! Getting default format ver. 4 - services = get_services(path, self._profile, 4 if self._profile is Profile.ENIGMA_2 else 0) + services = get_services(path, self._profile, 4 if self._profile is SettingsType.ENIGMA_2 else 0) for srv in services: self._services[srv.fav_id] = srv except FileNotFoundError as e: diff --git a/app/ui/iptv.py b/app/ui/iptv.py index 7693daeb..1c6705e2 100644 --- a/app/ui/iptv.py +++ b/app/ui/iptv.py @@ -13,7 +13,7 @@ from gi.repository import GLib from app.commons import run_idle, run_task, log from app.eparser.ecommons import BqServiceType, Service from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT -from app.settings import Profile +from app.settings import SettingsType from app.tools.yt import YouTube, PlayListParser from .dialogs import Action, show_dialog, DialogType, get_dialogs_string, get_message from .main_helper import get_base_model, get_iptv_url, on_popup_menu @@ -61,7 +61,7 @@ def get_yt_icon(icon_name, size=24): class IptvDialog: - def __init__(self, transient, view, services, bouquet, profile=Profile.ENIGMA_2, action=Action.ADD): + def __init__(self, transient, view, services, bouquet, profile=SettingsType.ENIGMA_2, action=Action.ADD): handlers = {"on_response": self.on_response, "on_entry_changed": self.on_entry_changed, "on_url_changed": self.on_url_changed, @@ -109,7 +109,7 @@ class IptvDialog: for el in self._digit_elems: el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) - if profile is Profile.NEUTRINO_MP: + if profile is SettingsType.NEUTRINO_MP: builder.get_object("iptv_dialog_ts_data_frame").set_visible(False) builder.get_object("iptv_type_label").set_visible(False) builder.get_object("reference_entry").set_visible(False) @@ -122,7 +122,7 @@ class IptvDialog: if self._action is Action.ADD: self._save_button.set_visible(False) self._add_button.set_visible(True) - if self._profile is Profile.ENIGMA_2: + if self._profile is SettingsType.ENIGMA_2: self._update_reference_entry() self._stream_type_combobox.set_active(1) elif self._action is Action.EDIT: @@ -147,13 +147,13 @@ class IptvDialog: if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL: return - self.save_enigma2_data() if self._profile is Profile.ENIGMA_2 else self.save_neutrino_data() + self.save_enigma2_data() if self._profile is SettingsType.ENIGMA_2 else self.save_neutrino_data() self._dialog.destroy() def init_data(self, srv): name, fav_id = srv[2], srv[7] self._name_entry.set_text(name) - self.init_enigma2_data(fav_id) if self._profile is Profile.ENIGMA_2 else self.init_neutrino_data(fav_id) + self.init_enigma2_data(fav_id) if self._profile is SettingsType.ENIGMA_2 else self.init_neutrino_data(fav_id) def init_enigma2_data(self, fav_id): data, sep, desc = fav_id.partition("#DESCRIPTION") @@ -190,7 +190,7 @@ class IptvDialog: self._description_entry.set_text(data[1]) def _update_reference_entry(self): - if self._profile is Profile.ENIGMA_2: + if self._profile is SettingsType.ENIGMA_2: self._reference_entry.set_text(_ENIGMA2_REFERENCE.format(self.get_type(), self._srv_type_entry.get_text(), int(self._sid_entry.get_text()), @@ -505,7 +505,7 @@ class IptvListConfigurationDialog: show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!") return - if self._profile is Profile.ENIGMA_2: + if self._profile is SettingsType.ENIGMA_2: reset = self._reset_to_default_switch.get_active() type_default = self._type_check_button.get_active() tid_default = self._tid_check_button.get_active() diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index 79b7c91e..e1c3897b 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -16,7 +16,7 @@ from app.eparser.ecommons import CAS, Flag, BouquetService from app.eparser.enigma.bouquets import BqServiceType from app.eparser.iptv import export_to_m3u from app.eparser.neutrino.bouquets import BqType -from app.settings import Profile, Settings +from app.settings import SettingsType, Settings, SettingsException from app.tools.media import Player from app.ui.epg_dialog import EpgDialog from app.ui.transmitter import LinksTransmitter @@ -158,8 +158,8 @@ class Application(Gtk.Application): "on_create_bouquet_for_each_type": self.on_create_bouquet_for_each_type} self._settings = Settings.get_instance() - self._profile = self._settings.profile - os.makedirs(os.path.dirname(self._settings.data_dir_path), exist_ok=True) + self._s_type = self._settings.setting_type + os.makedirs(os.path.dirname(self._settings.data_local_path), exist_ok=True) # Used for copy/paste. When adding the previous data will not be deleted. # Clearing only after the insertion! self._rows_buffer = [] @@ -188,7 +188,6 @@ class Application(Gtk.Application): self._EXTRA_COLOR = None # Color for services with a extra name for the bouquet builder = Gtk.Builder() - builder.set_translation_domain("demon-editor") builder.add_from_file(UI_RESOURCES_PATH + "main_window.glade") builder.connect_signals(handlers) self._main_window = builder.get_object("main_window") @@ -215,8 +214,8 @@ class Application(Gtk.Application): self._app_info_box.bind_property("visible", builder.get_object("right_header_box"), "sensitive", 4) self._app_info_box.bind_property("visible", builder.get_object("left_header_box"), "sensitive", 4) # Status bar - self._ip_label = builder.get_object("ip_label") - self._ip_label.set_text(self._settings.host) + self._profile_combo_box = builder.get_object("profile_combo_box") + self._profile_combo_box.set_tooltip_text(self._profile_combo_box.get_tooltip_text() + self._settings.host) self._receiver_info_box = builder.get_object("receiver_info_box") self._receiver_info_label = builder.get_object("receiver_info_label") self._signal_box = builder.get_object("signal_box") @@ -343,7 +342,7 @@ class Application(Gtk.Application): If update=False - first call on program start, else - after options changes! """ - if self._profile is Profile.ENIGMA_2: + if self._s_type is SettingsType.ENIGMA_2: if self._settings.use_colors: new_rgb = Gdk.RGBA() extra_rgb = Gdk.RGBA() @@ -574,7 +573,7 @@ class Application(Gtk.Application): # ***************** ####### *********************# def get_bouquet_file_name(self, bouquet): - bouquet_file_name = "{}userbouquet.{}.{}".format(self._settings.get(self._profile).get("data_dir_path"), + bouquet_file_name = "{}userbouquet.{}.{}".format(self._settings.get(self._s_type).get("data_dir_path"), *bouquet.split(":")) return bouquet_file_name @@ -824,11 +823,11 @@ class Application(Gtk.Application): @run_task def on_upload_data(self, download_type): try: - profile = self._profile + profile = self._s_type opts = self._settings - use_http = profile is Profile.ENIGMA_2 + use_http = profile is SettingsType.ENIGMA_2 - if profile is Profile.ENIGMA_2: + if profile is SettingsType.ENIGMA_2: host, port, user, password = opts.host, opts.http_port, opts.http_user, opts.http_password try: test_http(host, port, user, password, skip_message=True) @@ -858,17 +857,17 @@ class Application(Gtk.Application): self._wait_dialog.show() yield True - data_path = self._settings.data_dir_path if data_path is None else data_path + data_path = self._settings.data_local_path if data_path is None else data_path yield from self.clear_current_data() try: - prf = self._profile + prf = self._s_type black_list = get_blacklist(data_path) bouquets = get_bouquets(data_path, prf) yield True - services = get_services(data_path, prf, self.get_format_version() if prf is Profile.ENIGMA_2 else 0) + services = get_services(data_path, prf, self.get_format_version() if prf is SettingsType.ENIGMA_2 else 0) yield True - update_picons_data(self._settings.picons_dir_path, self._picons) + update_picons_data(self._settings.picons_local_path, self._picons) yield True except FileNotFoundError as e: msg = get_message("Please, download files from receiver or setup your path for read data!") @@ -1009,9 +1008,9 @@ class Application(Gtk.Application): def save_data(self): self._save_header_button.set_sensitive(False) - profile = self._profile - path = self._settings.data_dir_path - backup_path = self._settings.backup_dir_path + profile = self._s_type + path = self._settings.data_local_path + backup_path = self._settings.backup_local_path # Backup data or clearing data path backup_data(path, backup_path) if self._settings.backup_before_save else clear_data_path(path) yield True @@ -1031,7 +1030,7 @@ class Application(Gtk.Application): favs = self._bouquets[bq_id] ex_s = self._extra_bouquets.get(bq_id) bq_s = list(filter(None, [self._services.get(f_id, None) for f_id in favs])) - if profile is Profile.ENIGMA_2: + if profile is SettingsType.ENIGMA_2: bq_s = list(map(lambda s: s._replace(service=ex_s.get(s.fav_id, None) if ex_s else None), bq_s)) bq = Bouquet(bq_name, bq_type, bq_s, locked, hidden) bqs.append(bq) @@ -1045,10 +1044,10 @@ class Application(Gtk.Application): # Getting services services_model = get_base_model(self._services_view.get_model()) services = [Service(*row[: Column.SRV_TOOLTIP]) for row in services_model] - write_services(path, services, profile, self.get_format_version() if profile is Profile.ENIGMA_2 else 0) + write_services(path, services, profile, self.get_format_version() if profile is SettingsType.ENIGMA_2 else 0) yield True # removing bouquet files - if profile is Profile.ENIGMA_2: + if profile is SettingsType.ENIGMA_2: # blacklist write_blacklist(path, self._blacklist) @@ -1060,7 +1059,7 @@ class Application(Gtk.Application): if show_dialog(DialogType.QUESTION, self._main_window) == Gtk.ResponseType.CANCEL: return - gen = self.create_new_configuration(self._profile) + gen = self.create_new_configuration(self._s_type) GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) def create_new_configuration(self, profile): @@ -1070,12 +1069,12 @@ class Application(Gtk.Application): c_gen = self.clear_current_data() yield from c_gen - if profile is Profile.ENIGMA_2: + if profile is SettingsType.ENIGMA_2: parent = self._bouquets_model.append(None, ["Favourites (TV)", None, None, BqType.TV.value]) self.append_bouquet(Bouquet("Favourites (TV)", BqType.TV.value, [], None, None), parent) parent = self._bouquets_model.append(None, ["Favourites (Radio)", None, None, BqType.RADIO.value]) self.append_bouquet(Bouquet("Favourites (Radio)", BqType.RADIO.value, [], None, None), parent) - elif profile is Profile.NEUTRINO_MP: + elif profile is SettingsType.NEUTRINO_MP: self._bouquets_model.append(None, ["Providers", None, None, BqType.BOUQUET.value]) self._bouquets_model.append(None, ["FAV", None, None, BqType.TV.value]) self._bouquets_model.append(None, ["WEBTV", None, None, BqType.WEBTV.value]) @@ -1143,7 +1142,7 @@ class Application(Gtk.Application): self.show_error_dialog("Error. No bouquet is selected!") return - if self._profile is Profile.NEUTRINO_MP and self._bq_selected.endswith(BqType.WEBTV.value): + if self._s_type is SettingsType.NEUTRINO_MP and self._bq_selected.endswith(BqType.WEBTV.value): self.show_error_dialog("Operation not allowed in this context!") return @@ -1175,11 +1174,11 @@ class Application(Gtk.Application): GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) def update_options(self): - profile = self._settings.profile - self._ip_label.set_text(self._settings.host) - if profile != self._profile: + profile = self._settings.setting_type + + if profile != self._s_type: yield from self.show_app_info(True) - self._profile = profile + self._s_type = profile c_gen = self.clear_current_data() yield from c_gen self.update_profile_label() @@ -1284,7 +1283,7 @@ class Application(Gtk.Application): self._tool_elements[elem].set_sensitive(not_empty) if elem == "bouquets_paste_popup_item": self._tool_elements[elem].set_sensitive(not_empty and self._bouquets_buffer) - if self._profile is Profile.NEUTRINO_MP: + if self._s_type is SettingsType.NEUTRINO_MP: for elem in self._LOCK_HIDE_ELEMENTS: self._tool_elements[elem].set_sensitive(not_empty) else: @@ -1300,17 +1299,17 @@ class Application(Gtk.Application): for elem in self._BOUQUET_ELEMENTS: self._tool_elements[elem].set_sensitive(False) for elem in self._LOCK_HIDE_ELEMENTS: - self._tool_elements[elem].set_sensitive(not_empty and self._profile is Profile.ENIGMA_2) + self._tool_elements[elem].set_sensitive(not_empty and self._s_type is SettingsType.ENIGMA_2) for elem in self._FAV_IPTV_ELEMENTS: is_iptv = self._bq_selected and not is_service - if self._profile is Profile.NEUTRINO_MP: + if self._s_type is SettingsType.NEUTRINO_MP: is_iptv = is_iptv and BqType(self._bq_selected.split(":")[1]) is BqType.WEBTV self._tool_elements[elem].set_sensitive(is_iptv) for elem in self._COMMONS_ELEMENTS: self._tool_elements[elem].set_sensitive(not_empty) - if self._profile is not Profile.ENIGMA_2: + if self._s_type is not SettingsType.ENIGMA_2: for elem in self._FAV_ENIGMA_ELEMENTS: self._tool_elements[elem].set_sensitive(False) @@ -1321,9 +1320,9 @@ class Application(Gtk.Application): self.set_service_flags(Flag.LOCK) def set_service_flags(self, flag): - if self._profile is Profile.ENIGMA_2: + if self._s_type is SettingsType.ENIGMA_2: set_flags(flag, self._services_view, self._fav_view, self._services, self._blacklist) - elif self._profile is Profile.NEUTRINO_MP and self._bq_selected: + elif self._s_type is SettingsType.NEUTRINO_MP and self._bq_selected: model, paths = self._bouquets_view.get_selection().get_selected_rows() itr = model.get_iter(paths[0]) value = model.get_value(itr, 1 if flag is Flag.LOCK else 2) @@ -1386,14 +1385,14 @@ class Application(Gtk.Application): self._fav_view, self._services, self._bouquets.get(self._bq_selected, None), - self._profile, + self._s_type, Action.ADD).show() if response != Gtk.ResponseType.CANCEL: self.update_fav_num_column(self._fav_model) @run_idle def on_iptv_list_configuration(self, item): - if self._profile is Profile.NEUTRINO_MP: + if self._s_type is SettingsType.NEUTRINO_MP: self.show_error_dialog("Neutrino at the moment not supported!") return @@ -1407,7 +1406,7 @@ class Application(Gtk.Application): bq = self._bouquets.get(self._bq_selected, []) IptvListConfigurationDialog(self._main_window, self._services, iptv_rows, bq, - self._fav_model, self._profile).show() + self._fav_model, self._s_type).show() @run_idle def on_remove_all_unavailable(self, item): @@ -1423,7 +1422,7 @@ class Application(Gtk.Application): return fav_bqt = self._bouquets.get(self._bq_selected, None) - response = SearchUnavailableDialog(self._main_window, self._fav_model, fav_bqt, iptv_rows, self._profile).show() + response = SearchUnavailableDialog(self._main_window, self._fav_model, fav_bqt, iptv_rows, self._s_type).show() if response: next(self.remove_favs(response, self._fav_model), False) @@ -1431,7 +1430,7 @@ class Application(Gtk.Application): @run_idle def on_epg_list_configuration(self, item): - if self._profile is not Profile.ENIGMA_2: + if self._s_type is not SettingsType.ENIGMA_2: self.show_error_dialog("Only Enigma2 is supported!") return @@ -1449,7 +1448,7 @@ class Application(Gtk.Application): if not self._bq_selected: return - YtListImportDialog(self._main_window, self._profile, self.append_imported_services).show() + YtListImportDialog(self._main_window, self._s_type, self.append_imported_services).show() def on_import_m3u(self, item): """ Imports iptv from m3u files. """ @@ -1461,7 +1460,7 @@ class Application(Gtk.Application): self.show_error_dialog("No m3u file is selected!") return - channels = parse_m3u(response, self._profile) + channels = parse_m3u(response, self._s_type) if channels and self._bq_selected: self.append_imported_services(channels) @@ -1492,7 +1491,7 @@ class Application(Gtk.Application): try: bq = Bouquet(self._current_bq_name, None, bq_services, None, None) - export_to_m3u(response, bq, self._profile) + export_to_m3u(response, bq, self._s_type) except Exception as e: self.show_error_dialog(str(e)) else: @@ -1504,7 +1503,7 @@ class Application(Gtk.Application): self.show_error_dialog("No selected item!") return - appender = self.append_bouquet if self._profile is Profile.ENIGMA_2 else self.append_bouquets + appender = self.append_bouquet if self._s_type is SettingsType.ENIGMA_2 else self.append_bouquets import_bouquet(self._main_window, model, paths[0], self._settings, self._services, appender) def on_import_bouquets(self, item): @@ -1545,7 +1544,7 @@ class Application(Gtk.Application): self.show_error_dialog("Not allowed in this context!") return - url = get_iptv_url(row, self._profile) + url = get_iptv_url(row, self._s_type) self.update_player_buttons() if not url: return @@ -1659,10 +1658,11 @@ class Application(Gtk.Application): 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._profile is Profile.ENIGMA_2, not self._receiver_info_box.get_visible())) + 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) - if self._profile is Profile.NEUTRINO_MP or not http_api_enable: + if self._s_type is SettingsType.NEUTRINO_MP or not http_api_enable: self.update_info_boxes_visible(False) if self._http_api: self._http_api.close() @@ -1786,7 +1786,7 @@ class Application(Gtk.Application): self._sat_positions.clear() sat_positions = set() - if self._profile is Profile.ENIGMA_2: + if self._s_type is SettingsType.ENIGMA_2: terrestrial = False cable = False @@ -1803,7 +1803,7 @@ class Application(Gtk.Application): self._sat_positions.append("T") if cable: self._sat_positions.append("C") - elif self._profile is Profile.NEUTRINO_MP: + elif self._s_type is SettingsType.NEUTRINO_MP: list(map(lambda s: sat_positions.add(float(s.pos)), filter(lambda s: s.pos, self._services.values()))) self._sat_positions.extend(map(str, sorted(sat_positions))) @@ -1881,7 +1881,7 @@ class Application(Gtk.Application): self._fav_view, self._services, self._bouquets.get(self._bq_selected, None), - self._profile, + self._s_type, Action.EDIT).show() self.on_locate_in_services(view) @@ -1999,7 +1999,7 @@ class Application(Gtk.Application): @run_idle def on_picons_loader_show(self, item): ids = {} - if self._profile is Profile.ENIGMA_2: + if self._s_type is SettingsType.ENIGMA_2: for r in self._services_model: data = r[Column.SRV_PICON_ID].split("_") ids["{}:{}:{}".format(data[3], data[5], data[6])] = r[Column.SRV_PICON_ID] @@ -2009,7 +2009,7 @@ class Application(Gtk.Application): @run_task def update_picons(self): - update_picons_data(self._settings.picons_dir_path, self._picons) + update_picons_data(self._settings.picons_local_path, self._picons) append_picons(self._picons, self._services_model) def on_assign_picon(self, view): @@ -2062,15 +2062,20 @@ class Application(Gtk.Application): def create_bouquets(self, g_type): gen_bouquets(self._services_view, self._bouquets_view, self._main_window, g_type, self._TV_TYPES, - self._profile, self.append_bouquet) + self._s_type, self.append_bouquet) # ***************** Profile label *********************# def update_profile_label(self): - if self._profile is Profile.ENIGMA_2: - self._header_bar.set_subtitle("{} Enigma2 v.{}".format(get_message("Profile:"), self.get_format_version())) - elif self._profile is Profile.NEUTRINO_MP: - self._header_bar.set_subtitle("{} Neutrino-MP".format(get_message("Profile:"))) + label, sep, ip = self._profile_combo_box.get_tooltip_text().partition(":") + profile_name = self._profile_combo_box.get_active_text() + self._profile_combo_box.set_tooltip_text("{}: {}".format(label, self._settings.host)) + msg = get_message("Profile:") + + if self._s_type is SettingsType.ENIGMA_2: + self._header_bar.set_subtitle("{} {} [Enigma2 v.{}]".format(msg, profile_name, self.get_format_version())) + elif self._s_type is SettingsType.NEUTRINO_MP: + self._header_bar.set_subtitle("{} {} [Neutrino-MP]".format(msg, profile_name)) def get_format_version(self): return 5 if self._settings.v5_support else 4 @@ -2086,8 +2091,14 @@ class Application(Gtk.Application): def start_app(): - app = Application() - app.run(sys.argv) + try: + Settings.get_instance() + except SettingsException as e: + msg = "{} \n{}".format(e, "All setting were reset. Restart the program!") + show_dialog(DialogType.INFO, transient=Gtk.Dialog(), text=msg) + else: + app = Application() + app.run(sys.argv) if __name__ == "__main__": diff --git a/app/ui/main_helper.py b/app/ui/main_helper.py index bc874db9..39226f8d 100644 --- a/app/ui/main_helper.py +++ b/app/ui/main_helper.py @@ -9,7 +9,7 @@ from app.commons import run_task from app.eparser import Service from app.eparser.ecommons import Flag, BouquetService, Bouquet, BqType from app.eparser.enigma.bouquets import BqServiceType, to_bouquet_id -from app.settings import Profile +from app.settings import SettingsType from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog @@ -382,7 +382,7 @@ def assign_picon(target, srv_view, fav_view, transient, picons, settings, servic if picon_id: if os.path.isfile(response): - picons_path = settings.picons_dir_path + picons_path = settings.picons_local_path os.makedirs(os.path.dirname(picons_path), exist_ok=True) picon_file = picons_path + picon_id shutil.copy(response, picon_file) @@ -464,8 +464,8 @@ def remove_all_unused_picons(settings, picons, services): def remove_picons(settings, picon_ids, picons): - pions_path = settings.picons_dir_path - backup_path = settings.backup_dir_path + "picons/" + pions_path = settings.picons_local_path + backup_path = settings.backup_local_path + "picons/" os.makedirs(os.path.dirname(backup_path), exist_ok=True) for p_id in picon_ids: picons[p_id] = None @@ -492,7 +492,7 @@ def get_picon_pixbuf(path): # ***************** Bouquets *********************# -def gen_bouquets(view, bq_view, transient, gen_type, tv_types, profile, callback): +def gen_bouquets(view, bq_view, transient, gen_type, tv_types, s_type, callback): """ Auto-generate and append list of bouquets """ fav_id_index = Column.SRV_FAV_ID index = Column.SRV_TYPE @@ -502,7 +502,7 @@ def gen_bouquets(view, bq_view, transient, gen_type, tv_types, profile, callback index = Column.SRV_POS model, paths = view.get_selection().get_selected_rows() - bq_type = BqType.BOUQUET.value if profile is Profile.NEUTRINO_MP else BqType.TV.value + bq_type = BqType.BOUQUET.value if s_type is SettingsType.NEUTRINO_MP else BqType.TV.value if gen_type in (BqGenType.SAT, BqGenType.PACKAGE, BqGenType.TYPE): if not is_only_one_item_selected(paths, transient): return @@ -511,17 +511,17 @@ def gen_bouquets(view, bq_view, transient, gen_type, tv_types, profile, callback bq_type = BqType.RADIO.value append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model, [service.package if gen_type is BqGenType.PACKAGE else - service.pos if gen_type is BqGenType.SAT else service.service_type], profile) + service.pos if gen_type is BqGenType.SAT else service.service_type], s_type) else: wait_dialog = WaitDialog(transient) wait_dialog.show() append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model, - {row[index] for row in model}, profile, wait_dialog) + {row[index] for row in model}, s_type, wait_dialog) @run_task -def append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model, names, profile, wait_dialog=None): - bq_index = 0 if profile is Profile.ENIGMA_2 else 1 +def append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model, names, s_type, wait_dialog=None): + bq_index = 0 if s_type is SettingsType.ENIGMA_2 else 1 bq_view.expand_row(Gtk.TreePath(bq_index), 0) bqs_model = bq_view.get_model() bouquets_names = get_bouquets_names(bqs_model) @@ -583,14 +583,14 @@ def append_text_to_tview(char, view): view.scroll_to_mark(insert, 0.0, True, 0.0, 1.0) -def get_iptv_url(row, profile): +def get_iptv_url(row, s_type): """ Returns url from iptv type row """ - data = row[Column.FAV_ID].split(":" if profile is Profile.ENIGMA_2 else "::") - if profile is Profile.ENIGMA_2: + data = row[Column.FAV_ID].split(":" if s_type is SettingsType.ENIGMA_2 else "::") + if s_type is SettingsType.ENIGMA_2: data = list(filter(lambda x: "http" in x, data)) if data: url = data[0] - return urllib.request.unquote(url) if profile is Profile.ENIGMA_2 else url + return urllib.request.unquote(url) if s_type is SettingsType.ENIGMA_2 else url def on_popup_menu(menu, event): diff --git a/app/ui/main_window.glade b/app/ui/main_window.glade index 6daac2a0..b9eb54a4 100644 --- a/app/ui/main_window.glade +++ b/app/ui/main_window.glade @@ -26,7 +26,7 @@ THE SOFTWARE. Author: Dmitriy Yefremov --> - + @@ -961,6 +961,7 @@ Author: Dmitriy Yefremov 640 + 480 False center accessories-text-editor @@ -2798,7 +2799,7 @@ Author: Dmitriy Yefremov - 30 + 35 False @@ -2842,62 +2843,56 @@ Author: Dmitriy Yefremov - - True + False + False + Current IP: center - 2 - - - True - False - gtk-connect - - - False - True - 0 - - - - - True + center + 1 + 1 + 0 + False + True + + default + + + False + True center - Current IP: - - - + 1 + 1 + False + False + 9 + default + gtk-connect - - False - True - 1 - - - - - True - False - 127.0.0.1 - - - - - - False - True - 2 - - True + False True - 5 3 + + + False + No connection to the receiver + 10 + 10 + network-offline + + + False + True + end + 2 + + False @@ -2986,21 +2981,6 @@ Author: Dmitriy Yefremov 2 - - - False - No connection to the receiver - 10 - 10 - network-offline - - - False - True - end - 3 - - diff --git a/app/ui/picons_downloader.py b/app/ui/picons_downloader.py index 0ef967f0..6a343d61 100644 --- a/app/ui/picons_downloader.py +++ b/app/ui/picons_downloader.py @@ -9,7 +9,7 @@ from gi.repository import GLib, GdkPixbuf from app.commons import run_idle, run_task from app.connections import upload_data, DownloadType from app.tools.picons import PiconsParser, parse_providers, Provider, convert_to -from app.settings import Profile +from app.settings import SettingsType from app.tools.satellites import SatellitesParser, SatelliteSource from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, TV_ICON from .dialogs import show_dialog, DialogType, get_message @@ -86,13 +86,13 @@ class PiconsDialog: self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) self._settings = settings - self._profile = settings.profile + self._s_type = settings.setting_type self._ip_entry.set_text(self._settings.host) self._picons_entry.set_text(self._settings.picons_path) - self._picons_path = self._settings.picons_dir_path + self._picons_path = self._settings.picons_local_path self._picons_dir_entry.set_text(self._picons_path) - if not len(self._picon_ids) and self._profile is Profile.ENIGMA_2: + if not len(self._picon_ids) and self._s_type is SettingsType.ENIGMA_2: message = get_message("To automatically set the identifiers for picons,\n" "first load the required services list into the main application window.") self.show_info_message(message, Gtk.MessageType.WARNING) @@ -342,7 +342,7 @@ class PiconsDialog: self._expander.set_expanded(True) convert_to(src_path=picons_path, dest_path=save_path, - profile=Profile.ENIGMA_2, + s_type=SettingsType.ENIGMA_2, callback=self.append_output, done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)) @@ -362,10 +362,10 @@ class PiconsDialog: show_dialog(dialog_type, self._dialog, message) def get_picons_format(self): - picon_format = Profile.ENIGMA_2 + picon_format = SettingsType.ENIGMA_2 if self._neutrino_mp_radio_button.get_active(): - picon_format = Profile.NEUTRINO_MP + picon_format = SettingsType.NEUTRINO_MP return picon_format diff --git a/app/ui/satellites_dialog.py b/app/ui/satellites_dialog.py index 80fc0bc0..5da0656e 100644 --- a/app/ui/satellites_dialog.py +++ b/app/ui/satellites_dialog.py @@ -25,7 +25,7 @@ class SatellitesDialog: _aggr = [None for x in range(9)] # aggregate def __init__(self, transient, settings): - self._data_path = settings.data_dir_path + "satellites.xml" + self._data_path = settings.data_local_path + "satellites.xml" self._settings = settings handlers = {"on_open": self.on_open, diff --git a/app/ui/service_details_dialog.py b/app/ui/service_details_dialog.py index 0d2cffa0..3b2a8df5 100644 --- a/app/ui/service_details_dialog.py +++ b/app/ui/service_details_dialog.py @@ -6,7 +6,7 @@ from app.eparser import Service from app.eparser.ecommons import MODULATION, Inversion, ROLL_OFF, Pilot, Flag, Pids, POLARIZATION, \ get_key_by_value, get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION, TrType, \ SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, HIERARCHY, T_FEC -from app.settings import Profile +from app.settings import SettingsType from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON, Column, IS_GNOME_SESSION from .dialogs import show_dialog, DialogType, Action, get_dialogs_string from .main_helper import get_base_model @@ -52,10 +52,10 @@ class ServiceDetailsDialog: self._dialog = builder.get_object("service_details_dialog") self._dialog.set_transient_for(transient) - self._profile = settings.profile + self._s_type = settings.setting_type self._tr_type = None - self._satellites_xml_path = settings.data_dir_path + "satellites.xml" - self._picons_dir_path = settings.picons_dir_path + self._satellites_xml_path = settings.data_local_path + "satellites.xml" + self._picons_dir_path = settings.picons_local_path self._services_view = srv_view self._fav_view = fav_view self._action = action @@ -197,7 +197,7 @@ class ServiceDetailsDialog: self._package_entry.set_text(srv.package) self._sid_entry.set_text(str(int(srv.ssid, 16))) # Transponder - if self._profile is Profile.ENIGMA_2: + if self._s_type is SettingsType.ENIGMA_2: self._tr_type = TrType(srv.transponder_type) self._freq_entry.set_text(srv.freq) self._rate_entry.set_text(srv.rate) @@ -211,10 +211,10 @@ class ServiceDetailsDialog: else: self.set_sat_positions(srv.pos) - if self._profile is Profile.ENIGMA_2: + if self._s_type is SettingsType.ENIGMA_2: self.init_enigma2_service_data(srv) self.init_enigma2_transponder_data(srv) - elif self._profile is Profile.NEUTRINO_MP: + elif self._s_type is SettingsType.NEUTRINO_MP: self.init_neutrino_data(srv) self.init_neutrino_ui_elements() @@ -484,9 +484,9 @@ class ServiceDetailsDialog: transponder=transponder) def get_flags(self): - if self._profile is Profile.ENIGMA_2: + if self._s_type is SettingsType.ENIGMA_2: return self.get_enigma2_flags() - elif self._profile is Profile.NEUTRINO_MP: + elif self._s_type is SettingsType.NEUTRINO_MP: return self._old_service.flags_cas def get_enigma2_flags(self): @@ -532,12 +532,12 @@ class ServiceDetailsDialog: net_id, tr_id = int(self._network_id_entry.get_text()), int(self._transponder_id_entry.get_text()) service_type = self._srv_type_entry.get_text() - if self._profile is Profile.ENIGMA_2: + if self._s_type is SettingsType.ENIGMA_2: namespace = int(self._namespace_entry.get_text()) data_id = self._ENIGMA2_DATA_ID.format(ssid, namespace, tr_id, net_id, service_type, 0) fav_id = self._ENIGMA2_FAV_ID.format(ssid, tr_id, net_id, namespace) return fav_id, data_id - elif self._profile is Profile.NEUTRINO_MP: + elif self._s_type is SettingsType.NEUTRINO_MP: fav_id = self._NEUTRINO_FAV_ID.format(tr_id, net_id, ssid) return fav_id, self._old_service.data_id @@ -548,7 +548,7 @@ class ServiceDetailsDialog: fec = self._fec_combo_box.get_active_id() system = self._sys_combo_box.get_active_id() - if self._tr_type is TrType.Satellite or self._profile is Profile.NEUTRINO_MP: + if self._tr_type is TrType.Satellite or self._s_type is SettingsType.NEUTRINO_MP: freq = self._freq_entry.get_text() rate = self._rate_entry.get_text() pol = self._pol_combo_box.get_active_id() @@ -571,7 +571,7 @@ class ServiceDetailsDialog: inv = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id()) srv_sys = "0" # !!! - if self._profile is Profile.ENIGMA_2: + if self._s_type is SettingsType.ENIGMA_2: dvb_s_tr = self._ENIGMA2_TRANSPONDER_DATA.format("s", freq, rate, pol, fec, sat_pos, inv, srv_sys) if sys == "DVB-S": return dvb_s_tr @@ -585,7 +585,7 @@ class ServiceDetailsDialog: st_id = self._stream_id_entry.get_text() pls = ":{}:{}:{}".format(st_id, pls_code, pls_mode) if pls_mode and pls_code and st_id else "" return "{}:{}:{}:{}:{}{}".format(dvb_s_tr, flag, mod, roll_off, pilot, pls) - elif self._profile is Profile.NEUTRINO_MP: + elif self._s_type is SettingsType.NEUTRINO_MP: on_id, tr_id = int(self._network_id_entry.get_text()), int(self._transponder_id_entry.get_text()) mod = self.get_value_from_combobox_id(self._mod_combo_box, MODULATION) if sys == "DVB-S2" else None srv_sys = None @@ -682,7 +682,7 @@ class ServiceDetailsDialog: return True def update_reference(self, entry, event=None): - if not self.is_data_correct() or (event is None and self._profile is Profile.NEUTRINO_MP): + if not self.is_data_correct() or (event is None and self._s_type is SettingsType.NEUTRINO_MP): return self.update_reference_entry() @@ -691,7 +691,7 @@ class ServiceDetailsDialog: ssid = int(self._sid_entry.get_text()) tid = int(self._transponder_id_entry.get_text()) nid = int(self._network_id_entry.get_text()) - if self._profile is Profile.ENIGMA_2: + if self._s_type is SettingsType.ENIGMA_2: on_id = int(self._namespace_entry.get_text()) ref = "1:0:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0".format(srv_type, ssid, tid, nid, on_id) self._reference_entry.set_text(ref) diff --git a/app/ui/settings_dialog.glade b/app/ui/settings_dialog.glade index 9f2f65e1..8c949c1a 100644 --- a/app/ui/settings_dialog.glade +++ b/app/ui/settings_dialog.glade @@ -26,13 +26,23 @@ THE SOFTWARE. Author: Dmitriy Yefremov --> - + + + + + + + + + + + 1 10 @@ -47,7 +57,6 @@ Author: Dmitriy Yefremov 1 False - False True center-on-parent True @@ -59,6 +68,8 @@ Author: Dmitriy Yefremov True False + Options + Settings type: 2 True @@ -98,92 +109,6 @@ Author: Dmitriy Yefremov 2 - - - True - False - 10 - 10 - 5 - 5 - vertical - 5 - - - True - False - Options - - - - - - False - True - 0 - - - - - True - False - 10 - 10 - - - True - False - 2 - Profile: - - - False - True - 0 - - - - - Enigma2 - True - True - False - center - True - neutrino_radio_button - - - - False - True - 1 - - - - - Neutrino-MP - True - True - False - center - True - enigma_radio_button - - - False - True - 4 - - - - - False - True - 1 - - - - True @@ -254,74 +179,241 @@ Author: Dmitriy Yefremov False 5 - + True - False - 6 - 5 - vertical + True + True - - True + False + 5 + 5 0.019999999552965164 in - + + 155 True False - 5 - 5 + Set as default vertical 2 - + True - False - 5 - 2 - 2 - True + True + 1 + 1 + in - + True True - 127.0.0.1 - network-transmit-receive-symbolic + profile_lists_tore + False + 0 + + + + + + autosize + 70 + Profile + True + True + 0 + + + True + end + + + + 0 + + + + + + + fixed + Default + + + 5 + + + 1 + + + + + + + + + True + True + 0 + + + + + True + False + + + True + False + False + Add + Add + True + gtk-add + - 0 - 1 + False + True - + True False - Host: + Edit + Edit + True + gtk-edit + - 0 - 0 + False + True + + + + + True + False + Remove + Remove + True + gtk-remove + + + + False + True + + + + + True + False + + + + + + True + False + + + + + True + False + end + Set default + True + emblem-default + + + + False + True False True - 0 + 1 + + + + + True + False + Profile: + + + + + True + False + + + + + True + False + vertical + + + True + False + 5 + 5 + 5 + 0.019999999552965164 + in - + True False + 5 + 5 + 5 + vertical + 2 - + True False - center - settings_stack + 5 + 2 + 2 + True + + + True + False + center + 5 + Host: + + + 0 + 0 + + + + + True + True + 127.0.0.1 + network-transmit-receive-symbolic + + + 0 + 1 + + False @@ -330,59 +422,741 @@ Author: Dmitriy Yefremov - + True False - + + True + False + center + settings_stack + + + False + True + 0 + + + + + True + False + + + + + + True + True + 1 + + + + + True + False + + + False + True + 2 + + + + + Test + 110 + True + True + True + Test connection + test_button_image + True + + + + False + True + end + 3 + + + + + True + False + + + + + + True + True + 4 + - True + False True 1 - - True - False - - - False - True - 2 - - - - - Test - 110 - True - True - True - Test connection - test_button_image - True - - - - False - True - end - 3 - - - - + True False - + + True + False + 2 + 5 + 2 + 2 + + + True + False + Login: + + + 0 + 0 + + + + + True + True + True + root + avatar-default-symbolic + False + + + 0 + 1 + + + + + True + False + Password: + + + 1 + 0 + + + + + True + True + True + False + + root + emblem-readonly + False + password + + + 1 + 1 + + + + + True + True + 6 + 6 + 21 + network-workgroup-symbolic + + + 2 + 1 + + + + + + + + ftp + FTP + + + + + True + False + 2 + 5 + 2 + 2 + + + True + False + Login: + + + 0 + 0 + + + + + True + False + Password: + + + 1 + 0 + + + + + True + True + True + avatar-default-symbolic + + + 0 + 1 + + + + + True + False + Port: + + + 2 + 0 + + + + + True + True + 5 + 6 + 6 + 80 + network-workgroup-symbolic + + + 2 + 1 + + + + + True + True + True + False + + root + emblem-readonly + False + password + + + 1 + 1 + + + + + http + HTTP + 1 + + + + + True + False + 2 + 5 + 2 + 2 + + + True + False + Port: + + + 2 + 0 + + + + + True + True + 6 + 6 + 23 + network-workgroup-symbolic + + + 2 + 1 + + + + + True + False + Login: + + + 0 + 0 + + + + + True + True + True + 12 + 15 + avatar-default-symbolic + + + 0 + 1 + + + + + True + False + Password: + + + 1 + 0 + + + + + True + True + True + 12 + 15 + emblem-readonly + + + 1 + 1 + + + + + True + False + Timeout: + + + 3 + 0 + + + + + True + True + Timeout between commands in seconds + 2 + 6 + 6 + 1 + alarm-symbolic + number + telnet_timeout_adjustment + True + 1 + + + 3 + 1 + + + + + telnet + Telnet + 2 + - True + False + True + 1 + + + + + + + True + False + Network settings: + + + + + True + True + 0 + + + + + True + False + 5 + 5 + 0.019999999552965164 + in + + + True + False + 5 + 5 + 5 + 2 + True + + + True + False + Services and Bouquets files: + 0 + + + 0 + 0 + + + + + True + True + /etc/enigma2/ + gtk-edit + + + 0 + 1 + + + + + False + User bouquet files: + 2.2351741291171123e-10 + + + 0 + 2 + + + + + True + /etc/enigma2/ + gtk-edit + + + 0 + 3 + + + + + True + False + Satellites.xml file: + 0.019999999552965164 + + + 0 + 4 + + + + + True + True + /etc/tuxbox/ + gtk-edit + + + 0 + 5 + + + + + True + False + Picons: + 2.2351741291171123e-10 + + + 0 + 6 + + + + + True + True + /usr/share/enigma2/picon + gtk-edit + + + 0 + 7 + + + + + + + True + False + STB file paths: + + + + + True + True + 1 + + + + + True + False + 5 + 5 + 5 + 0.019999999552965164 + in + + + True + False + 5 + 5 + 5 + 2 + True + + + True + False + Picons path: + 0.019999999552965164 + + + 0 + 2 + + + + + True + True + /data/picons + gtk-edit + folder-open + False + Select + Select + + + + 0 + 3 + + + + + True + False + Data path: + 0 + 0.019999999552965164 + + + 0 + 0 + + + + + True + True + /data + gtk-edit + folder-open + False + Select + Select + + + + 0 + 1 + + + + + True + False + Backup path: + 0.019999999552965164 + + + 0 + 4 + + + + + True + True + /data/backup + gtk-edit + folder-open + False + Select + Select + + + + 0 + 5 + + + + + + + True + False + Local file paths: + + + + + True + True + 2 + + + + + True + False + + + + + profiles + Network + + + + + True + False + vertical + + + True + False + 5 + 5 + 5 + 5 + 0.019999999552965164 + in + + + True + False + 5 + 5 + 5 + 5 + + + True + False + Settings type: + + + False + True + 0 + + + + + True + False + center + center + 5 + + + Enigma2 + True + True + False + True + neutrino_radio_button + + + + False + True + 1 + + + + + Neutrino-MP + True + True + False + True + enigma_radio_button + + + False True 4 @@ -391,609 +1165,22 @@ Author: Dmitriy Yefremov False True - 1 - - - - - True - False - - - True - False - 2 - 5 - 2 - 2 - - - True - False - Login: - - - 0 - 0 - - - - - True - True - True - root - avatar-default-symbolic - False - - - 0 - 1 - - - - - True - False - Password: - - - 1 - 0 - - - - - True - True - True - False - - root - emblem-readonly - False - password - - - 1 - 1 - - - - - True - False - Port: - - - 2 - 0 - - - - - True - True - 6 - 6 - 21 - network-workgroup-symbolic - - - 2 - 1 - - - - - ftp - FTP - - - - - True - False - 2 - 5 - 2 - 2 - - - True - False - Login: - - - 0 - 0 - - - - - True - False - Password: - - - 1 - 0 - - - - - True - True - True - avatar-default-symbolic - - - 0 - 1 - - - - - True - False - Port: - - - 2 - 0 - - - - - True - True - 5 - 6 - 6 - 80 - network-workgroup-symbolic - - - 2 - 1 - - - - - True - True - True - False - - root - emblem-readonly - False - password - - - 1 - 1 - - - - - http - HTTP - 1 - - - - - True - False - 2 - 5 - 2 - 2 - - - True - False - Port: - - - 2 - 0 - - - - - True - True - 6 - 6 - 23 - network-workgroup-symbolic - - - 2 - 1 - - - - - True - False - Login: - - - 0 - 0 - - - - - True - True - True - 12 - 15 - avatar-default-symbolic - - - 0 - 1 - - - - - True - False - Password: - - - 1 - 0 - - - - - True - True - True - 12 - 15 - emblem-readonly - - - 1 - 1 - - - - - True - False - Timeout: - - - 3 - 0 - - - - - True - True - Timeout between commands in seconds - 2 - 6 - 6 - 1 - alarm-symbolic - number - telnet_timeout_adjustment - True - 1 - - - 3 - 1 - - - - - telnet - Telnet - 2 - - - - - False - True - 1 + end + 2 - - - True - False - Network settings: - + + - True + False True 0 - - - network - Network - - - - - True - False - 5 - 5 - vertical - - - True - False - 0.019999999552965164 - in - - - True - False - 5 - 5 - 5 - 2 - True - - - True - False - Services and Bouquets files: - 0 - - - 0 - 0 - - - - - True - True - /etc/enigma2/ - gtk-edit - - - 0 - 1 - - - - - False - User bouquet files: - 2.2351741291171123e-10 - - - 0 - 2 - - - - - True - /etc/enigma2/ - gtk-edit - - - 0 - 3 - - - - - True - False - Satellites.xml file: - 0.019999999552965164 - - - 0 - 4 - - - - - True - True - /etc/tuxbox/ - gtk-edit - - - 0 - 5 - - - - - True - False - Picons: - 2.2351741291171123e-10 - - - 0 - 6 - - - - - True - True - /usr/share/enigma2/picon - gtk-edit - - - 0 - 7 - - - - - - - True - False - STB file paths: - - - - - True - True - 0 - - - - - True - False - 5 - 0.019999999552965164 - in - - - True - False - 5 - 5 - 5 - 2 - True - - - True - False - Picons path: - 0.019999999552965164 - - - 0 - 2 - - - - - True - True - /data/picons - gtk-edit - folder-open - False - Select - - - - 0 - 3 - - - - - True - False - Data path: - 0 - 0.019999999552965164 - - - 0 - 0 - - - - - True - True - /data - gtk-edit - folder-open - False - Select - Select - - - - 0 - 1 - - - - - True - False - Backup path: - 0.019999999552965164 - - - 0 - 4 - - - - - True - True - /data/backup - gtk-edit - folder-open - False - Select - - - - 0 - 5 - - - - - - - True - False - Local file paths: - - - - - True - True - 1 - - - - - paths - Paths - 1 - - - - - True - False - vertical True @@ -1074,9 +1261,11 @@ Author: Dmitriy Yefremov + 250 True True True + end True @@ -1098,9 +1287,11 @@ Author: Dmitriy Yefremov + 250 True True True + end True @@ -1124,7 +1315,7 @@ Author: Dmitriy Yefremov False True - 0 + 1 @@ -1206,14 +1397,14 @@ Author: Dmitriy Yefremov False True - 1 + 2 program Program - 2 + 1 @@ -1525,7 +1716,7 @@ Author: Dmitriy Yefremov extra Extra - 3 + 2 @@ -1537,7 +1728,7 @@ Author: Dmitriy Yefremov - False + True True 0 diff --git a/app/ui/settings_dialog.py b/app/ui/settings_dialog.py index a86d7eb1..2376d8b9 100644 --- a/app/ui/settings_dialog.py +++ b/app/ui/settings_dialog.py @@ -2,8 +2,8 @@ from enum import Enum from app.commons import run_task, run_idle from app.connections import test_telnet, test_ftp, TestException, test_http -from app.settings import Profile -from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, FavClickMode +from app.settings import SettingsType +from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON from .main_helper import update_entry_data @@ -21,7 +21,7 @@ class SettingsDialog: def __init__(self, transient, settings): handlers = {"on_field_icon_press": self.on_field_icon_press, - "on_profile_changed": self.on_profile_changed, + "on_settings_type_changed": self.on_settings_type_changed, "on_reset": self.on_reset, "apply_settings": self.apply_settings, "on_connection_test": self.on_connection_test, @@ -29,10 +29,16 @@ class SettingsDialog: "on_set_color_switch_state": self.on_set_color_switch_state, "on_http_mode_switch_state": self.on_http_mode_switch_state, "on_yt_dl_switch_state": self.on_yt_dl_switch_state, - "on_send_to_switch_state": self.on_send_to_switch_state} + "on_send_to_switch_state": self.on_send_to_switch_state, + "on_profile_add": self.on_profile_add, + "on_profile_edit": self.on_profile_edit, + "on_profile_remove": self.on_profile_remove, + "on_profile_deleted": self.on_profile_deleted, + "on_profile_inserted": self.on_profile_inserted, + "on_profile_edited": self.on_profile_edited, + "on_profile_set_default": self.on_profile_set_default} builder = Gtk.Builder() - builder.set_translation_domain(TEXT_DOMAIN) builder.add_from_file(UI_RESOURCES_PATH + "settings_dialog.glade") builder.connect_signals(handlers) @@ -89,23 +95,38 @@ class SettingsDialog: self._click_mode_zap_button.bind_property("sensitive", self._enable_send_to_switch, "sensitive") self._enable_send_to_switch.bind_property("sensitive", builder.get_object("enable_send_to_label"), "sensitive") self._extra_support_grid.bind_property("sensitive", builder.get_object("v5_support_grid"), "sensitive") + # Profiles + self._profile_view = builder.get_object("profile_tree_view") + self._profile_remove_button = builder.get_object("profile_remove_button") + self._profile_view.get_model().append(("default", DEFAULT_ICON)) # Settings self._settings = settings - self._active_profile = settings.profile + self._profiles = settings.profiles + self._s_type = settings.setting_type self.set_settings() - self.init_ui_elements(self._active_profile) + self.init_ui_elements(self._s_type) - def init_ui_elements(self, profile): - is_enigma_profile = profile is Profile.ENIGMA_2 - self._neutrino_radio_button.set_active(profile is Profile.NEUTRINO_MP) + @run_idle + def init_ui_elements(self, s_type): + is_enigma_profile = s_type is SettingsType.ENIGMA_2 + self._neutrino_radio_button.set_active(s_type is SettingsType.NEUTRINO_MP) + self.update_header_bar() self._settings_stack.get_child_by_name(Property.HTTP.value).set_visible(is_enigma_profile) self._program_frame.set_sensitive(is_enigma_profile) self._extra_support_grid.set_sensitive(is_enigma_profile) http_active = self._support_http_api_switch.get_active() self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active) + self._profile_remove_button.set_sensitive(len(self._profile_view.get_model()) > 1) self.on_info_bar_close() if is_enigma_profile else self.show_info_message( "The Neutrino has only experimental support. Not all features are supported!", Gtk.MessageType.WARNING) + def update_header_bar(self): + label, sep, st = self._header_bar.get_subtitle().partition(":") + if self._s_type is SettingsType.ENIGMA_2: + self._header_bar.set_subtitle("{}: {}".format(label, self._enigma_radio_button.get_label())) + elif self._s_type is SettingsType.NEUTRINO_MP: + self._header_bar.set_subtitle("{}: {}".format(label, self._neutrino_radio_button.get_label())) + def show(self): response = self._dialog.run() if response == Gtk.ResponseType.OK: @@ -117,10 +138,10 @@ class SettingsDialog: def on_field_icon_press(self, entry, icon, event_button): update_entry_data(entry, self._dialog, self._settings) - def on_profile_changed(self, item): - profile = Profile.ENIGMA_2 if self._enigma_radio_button.get_active() else Profile.NEUTRINO_MP - self._active_profile = profile - self._settings.profile = profile + def on_settings_type_changed(self, item): + profile = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP + self._s_type = profile + self._settings.setting_type = profile self.set_settings() self.init_ui_elements(profile) @@ -144,14 +165,14 @@ class SettingsDialog: self._user_bouquet_field.set_text(self._settings.user_bouquet_path) self._satellites_xml_field.set_text(self._settings.satellites_xml_path) self._picons_field.set_text(self._settings.picons_path) - self._data_dir_field.set_text(self._settings.data_dir_path) - self._picons_dir_field.set_text(self._settings.picons_dir_path) - self._backup_dir_field.set_text(self._settings.backup_dir_path) + self._data_dir_field.set_text(self._settings.data_local_path) + self._picons_dir_field.set_text(self._settings.picons_local_path) + self._backup_dir_field.set_text(self._settings.backup_local_path) self._before_save_switch.set_active(self._settings.backup_before_save) self._before_downloading_switch.set_active(self._settings.backup_before_downloading) self.set_fav_click_mode(self._settings.fav_click_mode) - if self._active_profile is Profile.ENIGMA_2: + if self._s_type is SettingsType.ENIGMA_2: self._support_ver5_switch.set_active(self._settings.v5_support) self._support_http_api_switch.set_active(self._settings.http_api_support) self._enable_y_dl_switch.set_active(self._settings.enable_yt_dl) @@ -165,8 +186,8 @@ class SettingsDialog: self._extra_color_button.set_rgba(extra_rgb) def apply_settings(self, item=None): - self._active_profile = Profile.ENIGMA_2 if self._enigma_radio_button.get_active() else Profile.NEUTRINO_MP - self._settings.profile = self._active_profile + self._s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP + self._settings.setting_type = self._s_type self._settings.host = self._host_field.get_text() self._settings.port = self._port_field.get_text() self._settings.user = self._login_field.get_text() @@ -182,14 +203,14 @@ class SettingsDialog: self._settings.user_bouquet_path = self._user_bouquet_field.get_text() self._settings.satellites_xml_path = self._satellites_xml_field.get_text() self._settings.picons_path = self._picons_field.get_text() - self._settings.data_dir_path = self._data_dir_field.get_text() - self._settings.picons_dir_path = self._picons_dir_field.get_text() - self._settings.backup_dir_path = self._backup_dir_field.get_text() + self._settings.data_local_path = self._data_dir_field.get_text() + self._settings.picons_local_path = self._picons_dir_field.get_text() + self._settings.backup_local_path = self._backup_dir_field.get_text() self._settings.backup_before_save = self._before_save_switch.get_active() self._settings.backup_before_downloading = self._before_downloading_switch.get_active() self._settings.fav_click_mode = self.get_fav_click_mode() - if self._active_profile is Profile.ENIGMA_2: + if self._s_type is SettingsType.ENIGMA_2: self._settings.use_colors = self._set_color_switch.get_active() self._settings.new_color = self._new_color_button.get_rgba().to_string() self._settings.extra_color = self._extra_color_button.get_rgba().to_string() @@ -272,6 +293,42 @@ class SettingsDialog: def on_send_to_switch_state(self, switch, state): self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING) + def on_profile_add(self, item): + model = self._profile_view.get_model() + count = 0 + name = "profile" + while name in self._profiles: + count += 1 + name = "profile{}".format(count) + self._profiles[name] = {"host": self._host_field.get_text()} + itr = model.append((name, None)) + + def on_profile_edit(self, item): + self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING) + + def on_profile_remove(self, item): + model, paths = self._profile_view.get_selection().get_selected_rows() + if paths: + row = model[paths] + self._profiles.pop(row[0], None) + del model[paths] + + def on_profile_deleted(self, model, paths): + self._profile_remove_button.set_sensitive(len(model) > 1) + + def on_profile_edited(self, render, path, new_text): + p_name = render.get_property("text") + p_data = self._profiles.pop(p_name, None) + row = self._profile_view.get_model()[path] + row[0] = new_text + self._profiles[new_text] = p_data + + def on_profile_set_default(self, item): + self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING) + + def on_profile_inserted(self, model, path, itr): + self._profile_remove_button.set_sensitive(len(model) > 1) + @run_idle def set_fav_click_mode(self, mode): mode = FavClickMode(mode) diff --git a/app/ui/uicommons.py b/app/ui/uicommons.py index a222d354..20b28205 100644 --- a/app/ui/uicommons.py +++ b/app/ui/uicommons.py @@ -28,6 +28,7 @@ HIDE_ICON = theme.load_icon("go-jump", 16, 0) if theme.lookup_icon("go-jump", 16 TV_ICON = theme.load_icon("tv-symbolic", 16, 0) if theme.lookup_icon("tv-symbolic", 16, 0) else _IMAGE_MISSING IPTV_ICON = theme.load_icon("emblem-shared", 16, 0) if theme.lookup_icon("emblem-shared", 16, 0) else None EPG_ICON = theme.load_icon("gtk-index", 16, 0) if theme.lookup_icon("gtk-index", 16, 0) else None +DEFAULT_ICON = theme.load_icon("emblem-default", 16, 0) if theme.lookup_icon("emblem-default", 16, 0) else None class KeyboardKey(Enum): From 614c87cbf33387c541a6e70ee6ea4e0ee9b89c1a Mon Sep 17 00:00:00 2001 From: DYefremov Date: Fri, 27 Dec 2019 23:05:37 +0300 Subject: [PATCH 04/21] base implementation of profiles support --- app/connections.py | 17 ++- app/settings.py | 54 ++++++- app/ui/backup.py | 1 + app/ui/download_dialog.glade | 273 ++++++++++++++++++++--------------- app/ui/download_dialog.py | 35 ++++- app/ui/main_app_window.py | 81 +++++++---- app/ui/main_window.glade | 18 +-- app/ui/settings_dialog.glade | 9 +- app/ui/settings_dialog.py | 108 +++++++++++--- 9 files changed, 396 insertions(+), 200 deletions(-) diff --git a/app/connections.py b/app/connections.py index ea9cff51..2c0bf16b 100644 --- a/app/connections.py +++ b/app/connections.py @@ -11,7 +11,7 @@ from urllib.error import HTTPError, URLError from urllib.parse import urlencode from urllib.request import urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener, install_opener -from app.commons import log +from app.commons import log, run_idle from app.settings import SettingsType _BQ_FILES_LIST = ("tv", "radio", # enigma 2 @@ -277,12 +277,15 @@ def telnet(host, port=23, user="", password="", timeout=5): class HttpAPI: - def __init__(self, host, port, user, password): - self._base_url = "http://{}:{}/api/".format(host, port) - init_auth(user, password, self._base_url) + __MAX_WORKERS = 4 + + def __init__(self, settings): + self._settings = settings + self._base_url = None + self.init() from concurrent.futures import ThreadPoolExecutor as PoolExecutor - self._executor = PoolExecutor(max_workers=2) + self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS) def send(self, req_type, ref, callback=print): url = self._base_url + req_type.value @@ -295,6 +298,10 @@ class HttpAPI: future = self._executor.submit(get_json, req_type, url) future.add_done_callback(lambda f: callback(f.result())) + def init(self): + self._base_url = "http://{}:{}/api/".format(self._settings.host, self._settings.http_port) + init_auth(self._settings.http_user, self._settings.http_password, self._base_url) + def close(self): self._executor.shutdown(False) diff --git a/app/settings.py b/app/settings.py index 9fa3633a..18537a63 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,9 +1,10 @@ +import copy import json import os -from pprint import pformat -from textwrap import dedent from enum import Enum, IntEnum from pathlib import Path +from pprint import pformat +from textwrap import dedent CONFIG_PATH = str(Path.home()) + "/.config/demon-editor/" CONFIG_FILE = CONFIG_PATH + "config.json" @@ -25,11 +26,14 @@ class Defaults(Enum): FAV_CLICK_MODE = 0 -def get_default_settings(): +def get_default_settings(profile_name="default"): + def_settings = SettingsType.ENIGMA_2.get_default_settings() + set_local_paths(def_settings, profile_name) + return { "version": 1, "default_profile": Defaults.DEFAULT_PROFILE.value, - "profiles": {"default": SettingsType.ENIGMA_2.get_default_settings()}, + "profiles": {profile_name: def_settings}, "v5_support": Defaults.V5_SUPPORT.value, "http_api_support": Defaults.HTTP_API_SUPPORT.value, "enable_yt_dl": Defaults.ENABLE_YT_DL.value, @@ -41,6 +45,12 @@ def get_default_settings(): } +def set_local_paths(settings, profile_name): + settings["data_local_path"] = "{}{}/".format(settings["data_local_path"], profile_name) + settings["picons_local_path"] = "{}{}/".format(settings["picons_local_path"], profile_name) + settings["backup_local_path"] = "{}{}/".format(settings["backup_local_path"], profile_name) + + class SettingsType(IntEnum): """ Profiles for settings """ ENIGMA_2 = 0 @@ -49,13 +59,13 @@ class SettingsType(IntEnum): def get_default_settings(self): """ Returns default settings for current type """ if self is self.ENIGMA_2: - return {"setting_type": self, + return {"setting_type": self.value, "host": "127.0.0.1", "port": "21", "user": "root", "password": "root", "timeout": 5, "http_user": "root", "http_password": "", "http_port": "80", "http_timeout": 5, "telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 5, "services_path": "/etc/enigma2/", "user_bouquet_path": "/etc/enigma2/", "satellites_xml_path": "/etc/tuxbox/", "data_local_path": DATA_PATH + "enigma2/", - "picons_path": "/usr/share/enigma2/picon", + "picons_path": "/usr/share/enigma2/picon/", "picons_local_path": DATA_PATH + "enigma2/picons/", "backup_local_path": DATA_PATH + "enigma2/backup/"} elif self is self.NEUTRINO_MP: @@ -78,8 +88,8 @@ class Settings: __INSTANCE = None __VERSION = 1 - def __init__(self): - settings = get_settings() + def __init__(self, ext_settings=None): + settings = ext_settings or get_settings() if self.__VERSION > settings.get("version", 0): write_settings(get_default_settings()) @@ -112,6 +122,7 @@ class Settings: def reset(self, force_write=False): for k, v in self.setting_type.get_default_settings().items(): self._cp_settings[k] = v + set_local_paths(self._cp_settings, self._current_profile) if force_write: self.save() @@ -128,6 +139,33 @@ class Settings: """ Returns extra options or None """ return self._settings.get(name, None) + @property + def settings(self): + """ Returns copy of the current settings! """ + return copy.deepcopy(self._settings) + + @settings.setter + def settings(self, value): + """ Sets copy of the settings! """ + self._settings = copy.deepcopy(value) + + @property + def current_profile(self): + return self._current_profile + + @current_profile.setter + def current_profile(self, value): + self._current_profile = value + self._cp_settings = self._profiles.get(self._current_profile) + + @property + def default_profile(self): + return self._settings.get("default_profile", "default") + + @default_profile.setter + def default_profile(self, value): + self._settings["default_profile"] = value + @property def profiles(self): return self._profiles diff --git a/app/ui/backup.py b/app/ui/backup.py index 048ff058..5fb857b4 100644 --- a/app/ui/backup.py +++ b/app/ui/backup.py @@ -192,6 +192,7 @@ def backup_data(path, backup_path, move=True): """ backup_path = "{}{}/".format(backup_path, datetime.now().strftime("%Y-%m-%d_%H-%M-%S")) os.makedirs(os.path.dirname(backup_path), exist_ok=True) + os.makedirs(os.path.dirname(path), exist_ok=True) # backup files in data dir(skipping dirs and satellites.xml) for file in filter(lambda f: f != "satellites.xml" and os.path.isfile(os.path.join(path, f)), os.listdir(path)): src, dst = os.path.join(path, file), backup_path + file diff --git a/app/ui/download_dialog.glade b/app/ui/download_dialog.glade index a441afbd..00116fbf 100644 --- a/app/ui/download_dialog.glade +++ b/app/ui/download_dialog.glade @@ -26,7 +26,7 @@ THE SOFTWARE. Author: Dmitriy Yefremov --> - + @@ -47,6 +47,7 @@ Author: Dmitriy Yefremov True False + FTP-transfer 5 True @@ -56,7 +57,6 @@ Author: Dmitriy Yefremov 2 - 48 True True True @@ -78,7 +78,6 @@ Author: Dmitriy Yefremov - 48 True True True @@ -100,120 +99,13 @@ Author: Dmitriy Yefremov - - - True - False - 5 - 2 - vertical - 5 - - - True - False - FTP-transfer - - - False - True - 0 - - - - - True - False - 5 - 5 - - - True - False - 0 - - - False - True - 0 - - - - - All - True - True - False - True - satellites_radio_button - - - False - True - 1 - - - - - Bouquets - True - True - False - True - satellites_radio_button - - - False - True - 2 - - - - - Satellites - True - True - False - True - all_radio_button - - - False - True - 3 - - - - - WebTV - True - False - True - all_radio_button - - - False - True - 4 - - - - - False - True - 1 - - - - - 48 True True True Options - + True @@ -238,13 +130,156 @@ Author: Dmitriy Yefremov 1 vertical 2 + + + True + False + center + 10 + 5 + 5 + + + True + False + 0 + + + False + True + 0 + + + + + All + True + True + False + True + satellites_radio_button + + + False + True + 1 + + + + + Bouquets + True + True + False + True + satellites_radio_button + + + False + True + 2 + + + + + Satellites + True + True + False + True + all_radio_button + + + False + True + 3 + + + + + WebTV + True + False + True + all_radio_button + + + False + True + 4 + + + + + False + True + 0 + + + + + True + False + center + 5 + 5 + 5 + + + True + False + Profile: + + + False + True + 0 + + + + + True + False + False + center + 1 + 1 + 0 + False + True + + + + True + True + center + 1 + 1 + False + False + 9 + + + + + False + True + 1 + + + + + False + True + 1 + + True False 5 5 - 5 0.019999999552965164 in @@ -253,6 +288,7 @@ Author: Dmitriy Yefremov False 5 5 + 5 5 vertical @@ -319,7 +355,7 @@ Author: Dmitriy Yefremov False True - 0 + 1 @@ -398,7 +434,7 @@ Author: Dmitriy Yefremov False True - 0 + 2 @@ -590,7 +626,7 @@ Author: Dmitriy Yefremov False True - 1 + 3 @@ -629,7 +665,7 @@ Author: Dmitriy Yefremov False True - 3 + 4 @@ -685,9 +721,12 @@ Author: Dmitriy Yefremov False True - 4 + 5 + + + diff --git a/app/ui/download_dialog.py b/app/ui/download_dialog.py index 76fb06d6..5aa5a48f 100644 --- a/app/ui/download_dialog.py +++ b/app/ui/download_dialog.py @@ -1,3 +1,5 @@ +import os + from gi.repository import GLib from app.commons import run_idle, run_task @@ -6,8 +8,8 @@ from app.settings import SettingsType from app.ui.backup import backup_data, restore_data from app.ui.main_helper import append_text_to_tview from app.ui.settings_dialog import show_settings_dialog -from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN from .dialogs import show_dialog, DialogType, get_message +from .uicommons import Gtk, UI_RESOURCES_PATH class DownloadDialog: @@ -20,11 +22,11 @@ class DownloadDialog: handlers = {"on_receive": self.on_receive, "on_send": self.on_send, "on_settings_button": self.on_settings_button, - "on_preferences": self.on_preferences, + "on_settings": self.on_settings, + "on_profile_changed": self.on_profile_changed, "on_info_bar_close": self.on_info_bar_close} builder = Gtk.Builder() - builder.set_translation_domain(TEXT_DOMAIN) builder.add_from_file(UI_RESOURCES_PATH + "download_dialog.glade") builder.connect_signals(handlers) @@ -35,7 +37,6 @@ class DownloadDialog: self._message_label = builder.get_object("info_bar_message_label") self._text_view = builder.get_object("text_view") self._expander = builder.get_object("expander") - self._host_entry = builder.get_object("host_entry") self._data_path_entry = builder.get_object("data_path_entry") self._remove_unused_check_button = builder.get_object("remove_unused_check_button") @@ -52,12 +53,19 @@ class DownloadDialog: self._use_http_switch = builder.get_object("use_http_switch") self._http_radio_button = builder.get_object("http_radio_button") self._use_http_box = builder.get_object("use_http_box") + self._profile_combo_box = builder.get_object("profile_combo_box") + self.init_settings() def show(self): self._dialog_window.show() def init_settings(self): + self.update_profiles() + self.init_ui_settings() + + @run_idle + def init_ui_settings(self): self._host_entry.set_text(self._settings.host) self._data_path_entry.set_text(self._settings.data_local_path) is_enigma = self._s_type is SettingsType.ENIGMA_2 @@ -66,6 +74,12 @@ class DownloadDialog: self._use_http_box.set_visible(is_enigma) self._use_http_switch.set_active(is_enigma) + def update_profiles(self): + self._profile_combo_box.remove_all() + for p in self._settings.profiles: + self._profile_combo_box.append(p, p) + self._profile_combo_box.set_active_id(self._settings.current_profile) + @run_idle def on_receive(self, item): self.download(True, self.get_download_type()) @@ -108,11 +122,11 @@ class DownloadDialog: self._timeout_entry.set_text("") self._current_property = label - def on_preferences(self, item): + def on_settings(self, item): response = show_settings_dialog(self._dialog_window, self._settings) if response != Gtk.ResponseType.CANCEL: self._s_type = self._settings.setting_type - self.init_settings() + self.update_profiles() gen = self._update_settings_callback() GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) @@ -121,6 +135,13 @@ class DownloadDialog: self.on_settings_button(button) break + def on_profile_changed(self, box): + active = box.get_active_text() + if active in self._settings.profiles: + self._settings.current_profile = active + self._profile_combo_box.set_active_id(active) + self.init_ui_settings() + def on_info_bar_close(self, bar=None, resp=None): self._info_bar.set_visible(False) @@ -135,8 +156,10 @@ class DownloadDialog: if download: if backup and d_type is not DownloadType.SATELLITES: data_path = self._settings.data_local_path or self._data_path_entry.get_text() + os.makedirs(os.path.dirname(data_path), exist_ok=True) backup_path = self._settings.backup_local_path or data_path + "backup/" backup_src = backup_data(data_path, backup_path, d_type is DownloadType.ALL) + download_data(settings=self._settings, download_type=d_type, callback=self.append_output) else: self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO) diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index e1c3897b..c15a0124 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -1,6 +1,5 @@ import os import sys - from contextlib import suppress from functools import lru_cache from itertools import chain @@ -21,21 +20,21 @@ from app.tools.media import Player from app.ui.epg_dialog import EpgDialog from app.ui.transmitter import LinksTransmitter from .backup import BackupDialog, backup_data, clear_data_path -from .imports import ImportDialog, import_bouquet -from .download_dialog import DownloadDialog -from .iptv import IptvDialog, SearchUnavailableDialog, IptvListConfigurationDialog, YtListImportDialog -from .search import SearchProvider -from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON, IPTV_ICON, MOVE_KEYS, KeyboardKey, Column, \ - FavClickMode from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog, get_message +from .download_dialog import DownloadDialog +from .imports import ImportDialog, import_bouquet +from .iptv import IptvDialog, SearchUnavailableDialog, IptvListConfigurationDialog, YtListImportDialog from .main_helper import insert_marker, move_items, rename, ViewTarget, set_flags, locate_in_services, \ scroll_to, get_base_model, update_picons_data, copy_picon_reference, assign_picon, remove_picon, \ is_only_one_item_selected, gen_bouquets, BqGenType, get_iptv_url, append_picons, get_selection, get_model_data, \ remove_all_unused_picons from .picons_downloader import PiconsDialog from .satellites_dialog import show_satellites_dialog -from .settings_dialog import show_settings_dialog +from .search import SearchProvider from .service_details_dialog import ServiceDetailsDialog, Action +from .settings_dialog import show_settings_dialog +from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON, IPTV_ICON, MOVE_KEYS, KeyboardKey, Column, \ + FavClickMode class Application(Gtk.Application): @@ -75,7 +74,8 @@ class Application(Gtk.Application): handlers = {"on_close_app": self.on_close_app, "on_resize": self.on_resize, "on_about_app": self.on_about_app, - "on_preferences": self.on_preferences, + "on_settings": self.on_settings, + "on_profile_changed": self.on_profile_changed, "on_download": self.on_download, "on_data_open": self.on_data_open, "on_data_save": self.on_data_save, @@ -215,7 +215,6 @@ class Application(Gtk.Application): self._app_info_box.bind_property("visible", builder.get_object("left_header_box"), "sensitive", 4) # Status bar self._profile_combo_box = builder.get_object("profile_combo_box") - self._profile_combo_box.set_tooltip_text(self._profile_combo_box.get_tooltip_text() + self._settings.host) self._receiver_info_box = builder.get_object("receiver_info_box") self._receiver_info_label = builder.get_object("receiver_info_label") self._signal_box = builder.get_object("signal_box") @@ -288,10 +287,11 @@ class Application(Gtk.Application): def do_startup(self): Gtk.Application.do_startup(self) - self.update_profile_label() + self.init_profiles() self.init_drag_and_drop() self.init_colors() - self.init_http_api() + gen = self.init_http_api() + GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) def do_activate(self): self._main_window.set_application(self) @@ -315,6 +315,11 @@ class Application(Gtk.Application): self.activate() return 0 + @run_idle + def init_profiles(self): + self.update_profiles() + self._profile_combo_box.set_active_id(self._settings.default_profile) + def init_drag_and_drop(self): """ Enable drag-and-drop """ target = [] @@ -807,7 +812,7 @@ class Application(Gtk.Application): DownloadDialog(transient=self._main_window, settings=self._settings, open_data_callback=self.open_data, - update_settings_callback=self.update_options).show() + update_settings_callback=self.update_settings).show() @run_task def on_download_data(self): @@ -1167,25 +1172,46 @@ class Application(Gtk.Application): for v in [view, *args]: v.get_selection().unselect_all() - def on_preferences(self, item): + def on_settings(self, item): response = show_settings_dialog(self._main_window, self._settings) if response != Gtk.ResponseType.CANCEL: - gen = self.update_options() + gen = self.update_settings() GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) - def update_options(self): - profile = self._settings.setting_type + def update_settings(self): + s_type = self._settings.setting_type - if profile != self._s_type: + if s_type != self._s_type: yield from self.show_app_info(True) - self._s_type = profile + self._s_type = s_type c_gen = self.clear_current_data() yield from c_gen - self.update_profile_label() + self.init_colors(True) + self.init_profiles() yield True - self.init_http_api() - yield True + gen = self.init_http_api() + yield from gen + + def on_profile_changed(self, box): + if self._app_info_box.get_visible(): + self.update_profile_label() + return + + active = box.get_active_text() + if active in self._settings.profiles: + self._settings.current_profile = active + 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() + + def update_profiles(self): + self._profile_combo_box.remove_all() + for p in self._settings.profiles: + self._profile_combo_box.append(p, p) def on_tree_view_key_press(self, view, event): """ Handling keystrokes on press """ @@ -1654,7 +1680,7 @@ class Application(Gtk.Application): self._links_transmitter.hide() # ************************ HTTP API ****************************# - @run_task + def init_http_api(self): self._fav_click_mode = FavClickMode(self._settings.fav_click_mode) http_api_enable = self._settings.http_api_support @@ -1663,20 +1689,20 @@ class Application(Gtk.Application): GLib.idle_add(self._http_status_image.set_visible, status) if self._s_type is SettingsType.NEUTRINO_MP or not http_api_enable: - self.update_info_boxes_visible(False) + GLib.idle_add(self.update_info_boxes_visible, False) if self._http_api: self._http_api.close() + yield True self._http_api = None self.init_send_to(False) return if not self._http_api: - self._http_api = HttpAPI(self._settings.host, self._settings.http_port, - self._settings.http_user, self._settings.http_password) - + self._http_api = HttpAPI(self._settings) GLib.timeout_add_seconds(3, self.update_info, priority=GLib.PRIORITY_LOW) self.init_send_to(http_api_enable and self._settings.enable_send_to) + yield True @run_idle def init_send_to(self, enable): @@ -2066,6 +2092,7 @@ class Application(Gtk.Application): # ***************** Profile label *********************# + @run_idle def update_profile_label(self): label, sep, ip = self._profile_combo_box.get_tooltip_text().partition(":") profile_name = self._profile_combo_box.get_active_text() diff --git a/app/ui/main_window.glade b/app/ui/main_window.glade index b9eb54a4..4b95d72c 100644 --- a/app/ui/main_window.glade +++ b/app/ui/main_window.glade @@ -512,7 +512,7 @@ Author: Dmitriy Yefremov True 2 Settings - + True @@ -2718,7 +2718,6 @@ Author: Dmitriy Yefremov True False - 2 False @@ -2799,7 +2798,6 @@ Author: Dmitriy Yefremov - 35 False @@ -2844,6 +2842,7 @@ Author: Dmitriy Yefremov + True False False Current IP: @@ -2852,22 +2851,21 @@ Author: Dmitriy Yefremov 1 1 0 - False True - - default - + - + False True - center + baseline + baseline 1 1 False False 9 - default + True + False gtk-connect diff --git a/app/ui/settings_dialog.glade b/app/ui/settings_dialog.glade index 8c949c1a..7d756a91 100644 --- a/app/ui/settings_dialog.glade +++ b/app/ui/settings_dialog.glade @@ -64,6 +64,7 @@ Author: Dmitriy Yefremov True True center + True @@ -185,6 +186,7 @@ Author: Dmitriy Yefremov True + True False 5 5 @@ -195,7 +197,6 @@ Author: Dmitriy Yefremov 155 True False - Set as default vertical 2 @@ -212,6 +213,7 @@ Author: Dmitriy Yefremov profile_lists_tore False 0 + @@ -265,7 +267,6 @@ Author: Dmitriy Yefremov True - False False Add Add @@ -280,7 +281,6 @@ Author: Dmitriy Yefremov - True False Edit Edit @@ -988,7 +988,6 @@ Author: Dmitriy Yefremov True True - /data/picons gtk-edit folder-open False @@ -1018,7 +1017,6 @@ Author: Dmitriy Yefremov True True - /data gtk-edit folder-open False @@ -1047,7 +1045,6 @@ Author: Dmitriy Yefremov True True - /data/backup gtk-edit folder-open False diff --git a/app/ui/settings_dialog.py b/app/ui/settings_dialog.py index 2376d8b9..8347eeff 100644 --- a/app/ui/settings_dialog.py +++ b/app/ui/settings_dialog.py @@ -1,10 +1,13 @@ +import os from enum import Enum +from pathlib import Path from app.commons import run_task, run_idle from app.connections import test_telnet, test_ftp, TestException, test_http -from app.settings import SettingsType +from app.settings import SettingsType, Settings +from app.ui.dialogs import show_dialog, DialogType +from .main_helper import update_entry_data, scroll_to from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON -from .main_helper import update_entry_data def show_settings_dialog(transient, options): @@ -19,10 +22,11 @@ class Property(Enum): class SettingsDialog: - def __init__(self, transient, settings): + def __init__(self, transient, settings: Settings): handlers = {"on_field_icon_press": self.on_field_icon_press, "on_settings_type_changed": self.on_settings_type_changed, "on_reset": self.on_reset, + "on_response": self.on_response, "apply_settings": self.apply_settings, "on_connection_test": self.on_connection_test, "on_info_bar_close": self.on_info_bar_close, @@ -36,6 +40,7 @@ class SettingsDialog: "on_profile_deleted": self.on_profile_deleted, "on_profile_inserted": self.on_profile_inserted, "on_profile_edited": self.on_profile_edited, + "on_profile_selected": self.on_profile_selected, "on_profile_set_default": self.on_profile_set_default} builder = Gtk.Builder() @@ -70,7 +75,7 @@ class SettingsDialog: self._info_bar = builder.get_object("info_bar") self._message_label = builder.get_object("info_bar_message_label") self._test_spinner = builder.get_object("test_spinner") - # Profile + # Settings type self._enigma_radio_button = builder.get_object("enigma_radio_button") self._neutrino_radio_button = builder.get_object("neutrino_radio_button") self._support_ver5_switch = builder.get_object("support_ver5_switch") @@ -97,14 +102,15 @@ class SettingsDialog: self._extra_support_grid.bind_property("sensitive", builder.get_object("v5_support_grid"), "sensitive") # Profiles self._profile_view = builder.get_object("profile_tree_view") + self._profile_add_button = builder.get_object("profile_add_button") self._profile_remove_button = builder.get_object("profile_remove_button") - self._profile_view.get_model().append(("default", DEFAULT_ICON)) # Settings self._settings = settings - self._profiles = settings.profiles - self._s_type = settings.setting_type + self._profiles = self._settings.profiles + self._s_type = self._settings.setting_type self.set_settings() self.init_ui_elements(self._s_type) + self.init_profiles() @run_idle def init_ui_elements(self, s_type): @@ -116,10 +122,15 @@ class SettingsDialog: self._extra_support_grid.set_sensitive(is_enigma_profile) http_active = self._support_http_api_switch.get_active() self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active) - self._profile_remove_button.set_sensitive(len(self._profile_view.get_model()) > 1) self.on_info_bar_close() if is_enigma_profile else self.show_info_message( "The Neutrino has only experimental support. Not all features are supported!", Gtk.MessageType.WARNING) + def init_profiles(self): + p_def = self._settings.default_profile + for p in self._profiles: + self._profile_view.get_model().append((p, DEFAULT_ICON if p == p_def else None)) + self._profile_remove_button.set_sensitive(len(self._profile_view.get_model()) > 1) + def update_header_bar(self): label, sep, st = self._header_bar.get_subtitle().partition(":") if self._s_type is SettingsType.ENIGMA_2: @@ -128,12 +139,14 @@ class SettingsDialog: self._header_bar.set_subtitle("{}: {}".format(label, self._neutrino_radio_button.get_label())) def show(self): - response = self._dialog.run() - if response == Gtk.ResponseType.OK: - self.apply_settings() - self._dialog.destroy() + self._dialog.run() - return response + def on_response(self, dialog, resp): + if resp == Gtk.ResponseType.OK and not self.apply_settings(): + return + + self._dialog.destroy() + return resp def on_field_icon_press(self, entry, icon, event_button): update_entry_data(entry, self._dialog, self._settings) @@ -149,6 +162,7 @@ class SettingsDialog: self._settings.reset() self.set_settings() + @run_idle def set_settings(self): self._host_field.set_text(self._settings.host) self._port_field.set_text(self._settings.port) @@ -186,6 +200,9 @@ class SettingsDialog: self._extra_color_button.set_rgba(extra_rgb) def apply_settings(self, item=None): + if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL: + return + self._s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP self._settings.setting_type = self._s_type self._settings.host = self._host_field.get_text() @@ -219,7 +236,9 @@ class SettingsDialog: self._settings.enable_yt_dl = self._enable_y_dl_switch.get_active() self._settings.enable_send_to = self._enable_send_to_switch.get_active() + self._settings.default_profile = list(filter(lambda r: r[1], self._profile_view.get_model()))[0][0] self._settings.save() + return True @run_task def on_connection_test(self, item): @@ -300,8 +319,15 @@ class SettingsDialog: while name in self._profiles: count += 1 name = "profile{}".format(count) - self._profiles[name] = {"host": self._host_field.get_text()} - itr = model.append((name, None)) + + self._profiles[name] = self._s_type.get_default_settings() + model.append((name, None)) + scroll_to(len(model) - 1, self._profile_view) + self.on_profile_selected(self._profile_view) + p = name + "/" + self._settings.data_local_path += p + self._settings.picons_local_path += p + self._settings.backup_local_path += p def on_profile_edit(self, item): self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING) @@ -310,21 +336,61 @@ class SettingsDialog: model, paths = self._profile_view.get_selection().get_selected_rows() if paths: row = model[paths] + is_default = row[1] self._profiles.pop(row[0], None) del model[paths] + if is_default: + model.set_value(model.get_iter_first(), 1, DEFAULT_ICON) def on_profile_deleted(self, model, paths): self._profile_remove_button.set_sensitive(len(model) > 1) - def on_profile_edited(self, render, path, new_text): + def on_profile_edited(self, render, path, new_value): p_name = render.get_property("text") - p_data = self._profiles.pop(p_name, None) - row = self._profile_view.get_model()[path] - row[0] = new_text - self._profiles[new_text] = p_data + p_name = self._profiles.pop(p_name, None) + if p_name: + row = self._profile_view.get_model()[path] + row[0] = new_value + self._profiles[new_value] = p_name + if p_name == self._settings.current_profile: + self._settings.current_profile = new_value + if p_name == self._settings.default_profile: + self._settings.default_profile = new_value + + if p_name != new_value: + self.update_local_paths(new_value) + self.on_profile_selected(self._profile_view) + + def update_local_paths(self, profile_name): + data_path = self._settings.data_local_path + self._settings.data_local_path = "{}/{}/".format(Path(data_path).parent, profile_name) + if os.path.isdir(data_path): + os.rename(data_path, self._settings.data_local_path) + + picons_path = self._settings.picons_local_path + self._settings.picons_local_path = "{}/{}/".format(Path(picons_path).parent, profile_name) + if os.path.isdir(picons_path): + os.rename(picons_path, self._settings.picons_local_path) + + backup_path = self._settings.backup_local_path + self._settings.backup_local_path = "{}/{}/".format(Path(self._settings.backup_local_path).parent, profile_name) + if os.path.isdir(backup_path): + os.rename(backup_path, self._settings.backup_local_path) + + def on_profile_selected(self, view): + model, paths = self._profile_view.get_selection().get_selected_rows() + if paths: + profile = model.get_value(model.get_iter(paths), 0) + self._settings.current_profile = profile + self.set_settings() def on_profile_set_default(self, item): - self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING) + model, paths = self._profile_view.get_selection().get_selected_rows() + if paths: + itr = model.get_iter(paths) + model.foreach(lambda m, p, i: model.set_value(i, 1, None)) + model.set_value(itr, 1, DEFAULT_ICON) + self._settings.default_profile = model.get_value(itr, 0) def on_profile_inserted(self, model, path, itr): self._profile_remove_button.set_sensitive(len(model) > 1) From d0638f71589e91287763bab25422538dcaf1088c Mon Sep 17 00:00:00 2001 From: DYefremov Date: Thu, 2 Jan 2020 15:47:48 +0300 Subject: [PATCH 05/21] added language selection --- app/settings.py | 14 ++++++- app/ui/main_app_window.py | 1 + app/ui/settings_dialog.glade | 72 ++++++++++++++++++++++++++++++++++-- app/ui/settings_dialog.py | 39 ++++++++++++------- app/ui/uicommons.py | 16 +++++--- 5 files changed, 119 insertions(+), 23 deletions(-) diff --git a/app/settings.py b/app/settings.py index 18537a63..08425d15 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,5 +1,6 @@ import copy import json +import locale import os from enum import Enum, IntEnum from pathlib import Path @@ -92,7 +93,6 @@ class Settings: settings = ext_settings or get_settings() if self.__VERSION > settings.get("version", 0): - write_settings(get_default_settings()) raise SettingsException("Outdated version of the settings format!") self._settings = settings @@ -127,6 +127,10 @@ class Settings: if force_write: self.save() + @staticmethod + def reset_to_default(): + write_settings(get_default_settings()) + def get_default(self, p_name): """ Returns default value for current settings type """ return self.setting_type.get_default_settings().get(p_name) @@ -185,6 +189,14 @@ class Settings: for k, v in s_type.get_default_settings().items(): self._cp_settings[k] = v + @property + def language(self): + return self._settings.get("language", locale.getlocale()[0] or "en_US") + + @language.setter + def language(self, value): + self._settings["language"] = value + @property def host(self): return self._cp_settings.get("host", self.get_default("host")) diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index c15a0124..c066a3ff 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -2123,6 +2123,7 @@ def start_app(): except SettingsException as e: msg = "{} \n{}".format(e, "All setting were reset. Restart the program!") show_dialog(DialogType.INFO, transient=Gtk.Dialog(), text=msg) + Settings.reset_to_default() else: app = Application() app.run(sys.argv) diff --git a/app/ui/settings_dialog.glade b/app/ui/settings_dialog.glade index 7d756a91..022a8b00 100644 --- a/app/ui/settings_dialog.glade +++ b/app/ui/settings_dialog.glade @@ -1090,6 +1090,72 @@ Author: Dmitriy Yefremov True False vertical + + + True + False + 5 + 5 + 5 + 5 + 0 + in + + + True + False + 5 + 5 + 5 + 5 + + + True + False + Language: + + + False + True + 0 + + + + + 150 + True + False + False + 0 + + English + Deutsch + Español + Nederlands + Português + Русский + + + + + False + True + end + 1 + + + + + + + + + + False + True + 0 + + True @@ -1175,7 +1241,7 @@ Author: Dmitriy Yefremov False True - 0 + 1 @@ -1312,7 +1378,7 @@ Author: Dmitriy Yefremov False True - 1 + 2 @@ -1394,7 +1460,7 @@ Author: Dmitriy Yefremov False True - 2 + 3 diff --git a/app/ui/settings_dialog.py b/app/ui/settings_dialog.py index 8347eeff..d502c60c 100644 --- a/app/ui/settings_dialog.py +++ b/app/ui/settings_dialog.py @@ -41,7 +41,8 @@ class SettingsDialog: "on_profile_inserted": self.on_profile_inserted, "on_profile_edited": self.on_profile_edited, "on_profile_selected": self.on_profile_selected, - "on_profile_set_default": self.on_profile_set_default} + "on_profile_set_default": self.on_profile_set_default, + "on_lang_changed": self.on_lang_changed} builder = Gtk.Builder() builder.add_from_file(UI_RESOURCES_PATH + "settings_dialog.glade") @@ -104,6 +105,8 @@ class SettingsDialog: self._profile_view = builder.get_object("profile_tree_view") self._profile_add_button = builder.get_object("profile_add_button") self._profile_remove_button = builder.get_object("profile_remove_button") + # Language + self._lang_combo_box = builder.get_object("lang_combo_box") # Settings self._settings = settings self._profiles = self._settings.profiles @@ -122,6 +125,7 @@ class SettingsDialog: self._extra_support_grid.set_sensitive(is_enigma_profile) http_active = self._support_http_api_switch.get_active() self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active) + self._lang_combo_box.set_active_id(self._settings.language) self.on_info_bar_close() if is_enigma_profile else self.show_info_message( "The Neutrino has only experimental support. Not all features are supported!", Gtk.MessageType.WARNING) @@ -226,6 +230,7 @@ class SettingsDialog: self._settings.backup_before_save = self._before_save_switch.get_active() self._settings.backup_before_downloading = self._before_downloading_switch.get_active() self._settings.fav_click_mode = self.get_fav_click_mode() + self._settings.language = self._lang_combo_box.get_active_id() if self._s_type is SettingsType.ENIGMA_2: self._settings.use_colors = self._set_color_switch.get_active() @@ -339,6 +344,7 @@ class SettingsDialog: is_default = row[1] self._profiles.pop(row[0], None) del model[paths] + if is_default: model.set_value(model.get_iter_first(), 1, DEFAULT_ICON) @@ -361,21 +367,24 @@ class SettingsDialog: self.update_local_paths(new_value) self.on_profile_selected(self._profile_view) - def update_local_paths(self, profile_name): + def update_local_paths(self, p_name): data_path = self._settings.data_local_path - self._settings.data_local_path = "{}/{}/".format(Path(data_path).parent, profile_name) - if os.path.isdir(data_path): - os.rename(data_path, self._settings.data_local_path) - picons_path = self._settings.picons_local_path - self._settings.picons_local_path = "{}/{}/".format(Path(picons_path).parent, profile_name) - if os.path.isdir(picons_path): - os.rename(picons_path, self._settings.picons_local_path) - backup_path = self._settings.backup_local_path - self._settings.backup_local_path = "{}/{}/".format(Path(self._settings.backup_local_path).parent, profile_name) - if os.path.isdir(backup_path): - os.rename(backup_path, self._settings.backup_local_path) + + try: + if os.path.isdir(picons_path): + os.rename(picons_path, self._settings.picons_local_path) + if os.path.isdir(data_path): + os.rename(data_path, self._settings.data_local_path) + if os.path.isdir(backup_path): + os.rename(backup_path, self._settings.backup_local_path) + except OSError as e: + self.show_info_message(str(e), Gtk.MessageType.ERROR) + else: + self._settings.data_local_path = "{}/{}/".format(Path(data_path).parent, p_name) + self._settings.picons_local_path = "{}/{}/".format(Path(picons_path).parent, p_name) + self._settings.backup_local_path = "{}/{}/".format(Path(self._settings.backup_local_path).parent, p_name) def on_profile_selected(self, view): model, paths = self._profile_view.get_selection().get_selected_rows() @@ -395,6 +404,10 @@ class SettingsDialog: def on_profile_inserted(self, model, path, itr): self._profile_remove_button.set_sensitive(len(model) > 1) + def on_lang_changed(self, box): + if box.get_active_id() != self._settings.language: + self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING) + @run_idle def set_fav_click_mode(self, mode): mode = FavClickMode(mode) diff --git a/app/ui/uicommons.py b/app/ui/uicommons.py index 20b28205..4840e949 100644 --- a/app/ui/uicommons.py +++ b/app/ui/uicommons.py @@ -1,9 +1,9 @@ import locale import os +from enum import Enum, IntEnum +from app.settings import Settings, SettingsException import gi -from enum import Enum, IntEnum - gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk @@ -11,12 +11,16 @@ from gi.repository import Gtk, Gdk UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "/usr/share/demoneditor/app/ui/" IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID"))) - # translation TEXT_DOMAIN = "demon-editor" -if UI_RESOURCES_PATH == "app/ui/": - LANG_DIR = UI_RESOURCES_PATH + "lang" - locale.bindtextdomain(TEXT_DOMAIN, UI_RESOURCES_PATH + "lang") +try: + settings = Settings.get_instance() +except SettingsException: + pass +else: + os.environ["LANGUAGE"] = settings.language + if UI_RESOURCES_PATH == "app/ui/": + locale.bindtextdomain(TEXT_DOMAIN, UI_RESOURCES_PATH + "lang") theme = Gtk.IconTheme.get_default() _IMAGE_MISSING = theme.load_icon("image-missing", 16, 0) if theme.lookup_icon("image-missing", 16, 0) else None From bcbe2b3f4614b7272e13f39c6dd9b51967322f33 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Fri, 3 Jan 2020 23:26:55 +0300 Subject: [PATCH 06/21] added https support to the settings --- app/connections.py | 34 ++++++++++------ app/settings.py | 13 +++++- app/ui/main_app_window.py | 2 +- app/ui/settings_dialog.glade | 79 +++++++++++++++++++++++++----------- app/ui/settings_dialog.py | 20 ++++++++- 5 files changed, 108 insertions(+), 40 deletions(-) diff --git a/app/connections.py b/app/connections.py index 2c0bf16b..f9fc041b 100644 --- a/app/connections.py +++ b/app/connections.py @@ -11,7 +11,7 @@ from urllib.error import HTTPError, URLError from urllib.parse import urlencode from urllib.request import urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener, install_opener -from app.commons import log, run_idle +from app.commons import log from app.settings import SettingsType _BQ_FILES_LIST = ("tv", "radio", # enigma 2 @@ -97,7 +97,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(host, settings.http_port) + base_url = "http{}://{}:{}/api/".format("s" if settings.http_use_ssl else "", host, settings.http_port) tn, ht = None, None # telnet, http try: @@ -276,7 +276,6 @@ def telnet(host, port=23, user="", password="", timeout=5): # ***************** HTTP API *******************# class HttpAPI: - __MAX_WORKERS = 4 def __init__(self, settings): @@ -299,8 +298,10 @@ class HttpAPI: future.add_done_callback(lambda f: callback(f.result())) def init(self): - self._base_url = "http://{}:{}/api/".format(self._settings.host, self._settings.http_port) - init_auth(self._settings.http_user, self._settings.http_password, self._base_url) + 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) + init_auth(self._settings.http_user, self._settings.http_password, url, use_ssl) def close(self): self._executor.shutdown(False) @@ -313,7 +314,7 @@ def get_json(req_type, url): return f.read().decode("utf-8") else: return json.loads(f.read().decode("utf-8")) - except (URLError, HTTPError): + except (URLError, HTTPError, RemoteDisconnected): pass @@ -327,26 +328,35 @@ def test_ftp(host, port, user, password, timeout=5): raise TestException(e) -def test_http(host, port, user, password, timeout=5, skip_message=False): +def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message=False): try: params = urlencode({"text": "Connection test", "type": 2, "timeout": timeout}) params = "statusinfo" if skip_message else "message?{}".format(params) - url = "http://{}:{}/api/{}".format(host, port, params) + url = "http{}://{}:{}/api/".format("s" if use_ssl else "", host, port) # authentication - init_auth(user, password, url) + init_auth(user, password, url, use_ssl) - with urlopen(url, timeout=5) as f: + with urlopen("{}{}".format(url, params), timeout=5) as f: return json.loads(f.read().decode("utf-8")).get("message", "") except (RemoteDisconnected, URLError, HTTPError) as e: raise TestException(e) -def init_auth(user, password, url): +def init_auth(user, password, url, use_ssl=False): """ Init authentication """ pass_mgr = HTTPPasswordMgrWithDefaultRealm() pass_mgr.add_password(None, url, user, password) auth_handler = HTTPBasicAuthHandler(pass_mgr) - opener = build_opener(auth_handler) + + if use_ssl: + import ssl + from urllib.request import HTTPSHandler + # https://stackoverflow.com/a/28052583 + context = ssl._create_unverified_context() + opener = build_opener(auth_handler, HTTPSHandler(context=context)) + else: + opener = build_opener(auth_handler) + install_opener(opener) diff --git a/app/settings.py b/app/settings.py index 08425d15..ccbadb1f 100644 --- a/app/settings.py +++ b/app/settings.py @@ -62,7 +62,8 @@ class SettingsType(IntEnum): if self is self.ENIGMA_2: return {"setting_type": self.value, "host": "127.0.0.1", "port": "21", "user": "root", "password": "root", "timeout": 5, - "http_user": "root", "http_password": "", "http_port": "80", "http_timeout": 5, + "http_user": "root", "http_password": "", "http_port": "80", + "http_timeout": 5, "http_use_ssl": False, "telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 5, "services_path": "/etc/enigma2/", "user_bouquet_path": "/etc/enigma2/", "satellites_xml_path": "/etc/tuxbox/", "data_local_path": DATA_PATH + "enigma2/", @@ -72,7 +73,7 @@ class SettingsType(IntEnum): elif self is self.NEUTRINO_MP: return {"setting_type": self, "host": "127.0.0.1", "port": "21", "user": "root", "password": "root", "timeout": 5, - "http_user": "", "http_password": "", "http_port": "80", "http_timeout": 2, + "http_user": "", "http_password": "", "http_port": "80", "http_timeout": 2, "http_use_ssl": False, "telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 1, "services_path": "/var/tuxbox/config/zapit/", "user_bouquet_path": "/var/tuxbox/config/zapit/", "satellites_xml_path": "/var/tuxbox/config/", "data_local_path": DATA_PATH + "neutrino/", @@ -261,6 +262,14 @@ class Settings: def http_timeout(self, value): self._cp_settings["http_timeout"] = value + @property + def http_use_ssl(self): + return self._cp_settings.get("http_use_ssl", self.get_default("http_use_ssl")) + + @http_use_ssl.setter + def http_use_ssl(self, value): + self._cp_settings["http_use_ssl"] = value + @property def telnet_user(self): return self._cp_settings.get("telnet_user", self.get_default("telnet_user")) diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index c066a3ff..3406894a 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -835,7 +835,7 @@ class Application(Gtk.Application): if profile is SettingsType.ENIGMA_2: host, port, user, password = opts.host, opts.http_port, opts.http_user, opts.http_password try: - test_http(host, port, user, password, skip_message=True) + test_http(host, port, user, password, use_ssl=opts.http_use_ssl, skip_message=True) except TestException: use_http = False diff --git a/app/ui/settings_dialog.glade b/app/ui/settings_dialog.glade index 022a8b00..0388972b 100644 --- a/app/ui/settings_dialog.glade +++ b/app/ui/settings_dialog.glade @@ -162,7 +162,7 @@ Author: Dmitriy Yefremov True False - + 150 True False @@ -378,7 +378,6 @@ Author: Dmitriy Yefremov False 5 5 - 5 vertical 2 @@ -389,19 +388,6 @@ Author: Dmitriy Yefremov 2 2 True - - - True - False - center - 5 - Host: - - - 0 - 0 - - True @@ -414,6 +400,48 @@ Author: Dmitriy Yefremov 1 + + + 22 + True + False + 2 + + + True + False + end + 5 + Host: + + + False + True + 0 + + + + + SSL/TSL + False + False + end + True + + + + False + False + end + 1 + + + + + 0 + 0 + + False @@ -425,6 +453,7 @@ Author: Dmitriy Yefremov True False + 5 True @@ -507,12 +536,12 @@ Author: Dmitriy Yefremov True False + 5 + True False - 2 - 5 2 2 @@ -583,7 +612,15 @@ Author: Dmitriy Yefremov - + + True + False + Port: + + + 2 + 0 + @@ -595,8 +632,6 @@ Author: Dmitriy Yefremov True False - 2 - 5 2 2 @@ -687,8 +722,6 @@ Author: Dmitriy Yefremov True False - 2 - 5 2 2 @@ -808,7 +841,7 @@ Author: Dmitriy Yefremov False True - 1 + 2 diff --git a/app/ui/settings_dialog.py b/app/ui/settings_dialog.py index d502c60c..52bb5be1 100644 --- a/app/ui/settings_dialog.py +++ b/app/ui/settings_dialog.py @@ -42,7 +42,9 @@ class SettingsDialog: "on_profile_edited": self.on_profile_edited, "on_profile_selected": self.on_profile_selected, "on_profile_set_default": self.on_profile_set_default, - "on_lang_changed": self.on_lang_changed} + "on_lang_changed": self.on_lang_changed, + "on_current_settings_visible": self.on_current_settings_visible, + "on_http_use_ssl_toggled": self.on_http_use_ssl_toggled} builder = Gtk.Builder() builder.add_from_file(UI_RESOURCES_PATH + "settings_dialog.glade") @@ -59,6 +61,7 @@ class SettingsDialog: self._http_login_field = builder.get_object("http_login_field") self._http_password_field = builder.get_object("http_password_field") self._http_port_field = builder.get_object("http_port_field") + self._http_use_ssl_check_button = builder.get_object("http_use_ssl_check_button") self._telnet_login_field = builder.get_object("telnet_login_field") self._telnet_password_field = builder.get_object("telnet_password_field") self._telnet_port_field = builder.get_object("telnet_port_field") @@ -175,6 +178,7 @@ class SettingsDialog: self._http_login_field.set_text(self._settings.http_user) self._http_password_field.set_text(self._settings.http_password) self._http_port_field.set_text(self._settings.http_port) + self._http_use_ssl_check_button.set_active(self._settings.http_use_ssl) self._telnet_login_field.set_text(self._settings.telnet_user) self._telnet_password_field.set_text(self._settings.telnet_password) self._telnet_port_field.set_text(self._settings.telnet_port) @@ -216,6 +220,7 @@ class SettingsDialog: self._settings.http_user = self._http_login_field.get_text() self._settings.http_password = self._http_password_field.get_text() self._settings.http_port = self._http_port_field.get_text() + self._settings.http_use_ssl = self._http_use_ssl_check_button.get_active() self._settings.telnet_user = self._telnet_login_field.get_text() self._settings.telnet_password = self._telnet_password_field.get_text() self._settings.telnet_port = self._telnet_port_field.get_text() @@ -261,8 +266,9 @@ class SettingsDialog: def test_http(self): user, password = self._http_login_field.get_text(), self._http_password_field.get_text() host, port = self._host_field.get_text(), self._http_port_field.get_text() + use_ssl = self._http_use_ssl_check_button.get_active() try: - self.show_info_message(test_http(host, port, user, password), Gtk.MessageType.INFO) + self.show_info_message(test_http(host, port, user, password, use_ssl=use_ssl), Gtk.MessageType.INFO) except TestException as e: self.show_info_message(str(e), Gtk.MessageType.ERROR) finally: @@ -408,6 +414,16 @@ class SettingsDialog: if box.get_active_id() != self._settings.language: self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING) + def on_current_settings_visible(self, stack: Gtk.Stack, param): + self._http_use_ssl_check_button.set_visible(Property(stack.get_visible_child_name()) is Property.HTTP) + + def on_http_use_ssl_toggled(self, button): + active = button.get_active() + self._settings.http_use_ssl = active + port = "443" if active else "80" + self._http_port_field.set_text(port) + self._settings.http_port = port + @run_idle def set_fav_click_mode(self, mode): mode = FavClickMode(mode) From 1d62e2660be8635811e891dee4bf275d3bb0a568 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Mon, 6 Jan 2020 13:17:56 +0300 Subject: [PATCH 07/21] added http api compatibility test --- app/connections.py | 31 ++++++++++++++++++++++++------- app/ui/main_app_window.py | 4 ++-- app/ui/settings_dialog.py | 4 +++- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/app/connections.py b/app/connections.py index f9fc041b..12e75472 100644 --- a/app/connections.py +++ b/app/connections.py @@ -45,6 +45,10 @@ class TestException(Exception): pass +class HttpApiException(Exception): + pass + + def download_data(*, settings, download_type=DownloadType.ALL, callback=print): with FTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp: ftp.encoding = "utf-8" @@ -329,17 +333,30 @@ def test_ftp(host, port, user, password, timeout=5): def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message=False): + params = urlencode({"text": "Connection test", "type": 2, "timeout": timeout}) + params = "statusinfo" if skip_message else "message?{}".format(params) + base_url = "http{}://{}:{}".format("s" if use_ssl else "", host, port) + # authentication + init_auth(user, password, base_url, use_ssl) try: - params = urlencode({"text": "Connection test", "type": 2, "timeout": timeout}) - params = "statusinfo" if skip_message else "message?{}".format(params) - url = "http{}://{}:{}/api/".format("s" if use_ssl else "", host, port) - # authentication - init_auth(user, password, url, use_ssl) - - with urlopen("{}{}".format(url, params), timeout=5) as f: + 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 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): diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index 3406894a..ba900dd7 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -8,7 +8,7 @@ from gi.repository import GLib, Gio from app.commons import run_idle, log, run_task, run_with_delay, init_logger from app.connections import HttpAPI, HttpRequestType, download_data, DownloadType, upload_data, test_http, \ - TestException + TestException, HttpApiException from app.eparser import get_blacklist, write_blacklist, parse_m3u from app.eparser import get_services, get_bouquets, write_bouquets, write_services, Bouquets, Bouquet, Service from app.eparser.ecommons import CAS, Flag, BouquetService @@ -836,7 +836,7 @@ class Application(Gtk.Application): host, port, user, password = opts.host, opts.http_port, opts.http_user, opts.http_password try: test_http(host, port, user, password, use_ssl=opts.http_use_ssl, skip_message=True) - except TestException: + except (TestException, HttpApiException): use_http = False upload_data(settings=opts, diff --git a/app/ui/settings_dialog.py b/app/ui/settings_dialog.py index 52bb5be1..cf06e22c 100644 --- a/app/ui/settings_dialog.py +++ b/app/ui/settings_dialog.py @@ -3,7 +3,7 @@ from enum import Enum from pathlib import Path from app.commons import run_task, run_idle -from app.connections import test_telnet, test_ftp, TestException, test_http +from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException from app.settings import SettingsType, Settings from app.ui.dialogs import show_dialog, DialogType from .main_helper import update_entry_data, scroll_to @@ -271,6 +271,8 @@ class SettingsDialog: self.show_info_message(test_http(host, port, user, password, use_ssl=use_ssl), Gtk.MessageType.INFO) except TestException as e: self.show_info_message(str(e), Gtk.MessageType.ERROR) + except HttpApiException as e: + self.show_info_message(str(e), Gtk.MessageType.WARNING) finally: self.show_spinner(False) From 0571a29b66f3ea5a5d19d149cde122b90b9dd658 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Tue, 7 Jan 2020 12:36:29 +0300 Subject: [PATCH 08/21] added elems to profiles edit --- app/ui/main_app_window.py | 7 ++- app/ui/main_window.glade | 2 +- app/ui/settings_dialog.glade | 107 ++++++++++++++++++++++++++++++++++- app/ui/settings_dialog.py | 97 +++++++++++++++++-------------- 4 files changed, 165 insertions(+), 48 deletions(-) diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index ba900dd7..39aa4202 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -315,7 +315,6 @@ class Application(Gtk.Application): self.activate() return 0 - @run_idle def init_profiles(self): self.update_profiles() self._profile_combo_box.set_active_id(self._settings.default_profile) @@ -1193,12 +1192,12 @@ class Application(Gtk.Application): gen = self.init_http_api() yield from gen - def on_profile_changed(self, box): + def on_profile_changed(self, entry): if self._app_info_box.get_visible(): self.update_profile_label() return - active = box.get_active_text() + active = self._profile_combo_box.get_active_text() if active in self._settings.profiles: self._settings.current_profile = active self._s_type = self._settings.setting_type @@ -1208,6 +1207,8 @@ class Application(Gtk.Application): if self._http_api and self._settings.http_api_support: self._http_api.init() + self.open_data() + def update_profiles(self): self._profile_combo_box.remove_all() for p in self._settings.profiles: diff --git a/app/ui/main_window.glade b/app/ui/main_window.glade index 4b95d72c..64b45d84 100644 --- a/app/ui/main_window.glade +++ b/app/ui/main_window.glade @@ -2852,7 +2852,6 @@ Author: Dmitriy Yefremov 1 0 True - False @@ -2867,6 +2866,7 @@ Author: Dmitriy Yefremov True False gtk-connect + diff --git a/app/ui/settings_dialog.glade b/app/ui/settings_dialog.glade index 0388972b..e8942154 100644 --- a/app/ui/settings_dialog.glade +++ b/app/ui/settings_dialog.glade @@ -43,6 +43,71 @@ Author: Dmitriy Yefremov + + True + False + emblem-default + + + True + False + + + gtk-add + True + False + True + True + + + + + + + gtk-edit + True + False + True + True + + + + + + + True + False + + + + + Set default + True + False + set_default_image + False + + + + + + + True + False + + + + + gtk-remove + True + False + True + True + + + + + 1 10 @@ -85,7 +150,7 @@ Author: Dmitriy Yefremov True False - gtk-apply + gtk-ok @@ -110,6 +175,37 @@ Author: Dmitriy Yefremov 2 + + + True + False + 1 + 1 + + + 3 + + + + + True + True + True + Apply profile settings + + + + True + False + gtk-apply + + + + + + 4 + + True @@ -179,6 +275,7 @@ Author: Dmitriy Yefremov True False 5 + True @@ -213,6 +310,7 @@ Author: Dmitriy Yefremov profile_lists_tore False 0 + @@ -273,6 +371,7 @@ Author: Dmitriy Yefremov True gtk-add + False @@ -281,12 +380,14 @@ Author: Dmitriy Yefremov + True False Edit Edit True gtk-edit + False @@ -302,6 +403,7 @@ Author: Dmitriy Yefremov True gtk-remove + False @@ -330,6 +432,7 @@ Author: Dmitriy Yefremov True emblem-default + False @@ -537,7 +640,7 @@ Author: Dmitriy Yefremov True False 5 - + True diff --git a/app/ui/settings_dialog.py b/app/ui/settings_dialog.py index cf06e22c..996056ad 100644 --- a/app/ui/settings_dialog.py +++ b/app/ui/settings_dialog.py @@ -28,6 +28,7 @@ class SettingsDialog: "on_reset": self.on_reset, "on_response": self.on_response, "apply_settings": self.apply_settings, + "on_apply_profile_settings": self.on_apply_profile_settings, "on_connection_test": self.on_connection_test, "on_info_bar_close": self.on_info_bar_close, "on_set_color_switch_state": self.on_set_color_switch_state, @@ -43,8 +44,10 @@ class SettingsDialog: "on_profile_selected": self.on_profile_selected, "on_profile_set_default": self.on_profile_set_default, "on_lang_changed": self.on_lang_changed, - "on_current_settings_visible": self.on_current_settings_visible, - "on_http_use_ssl_toggled": self.on_http_use_ssl_toggled} + "on_main_settings_visible": self.on_main_settings_visible, + "on_network_settings_visible": self.on_network_settings_visible, + "on_http_use_ssl_toggled": self.on_http_use_ssl_toggled, + "on_view_popup_menu": self.on_view_popup_menu} builder = Gtk.Builder() builder.add_from_file(UI_RESOURCES_PATH + "settings_dialog.glade") @@ -108,10 +111,13 @@ class SettingsDialog: self._profile_view = builder.get_object("profile_tree_view") self._profile_add_button = builder.get_object("profile_add_button") self._profile_remove_button = builder.get_object("profile_remove_button") + self._apply_profile_button = builder.get_object("apply_profile_button") + self._apply_profile_button.bind_property("visible", builder.get_object("header_separator"), "visible") # Language self._lang_combo_box = builder.get_object("lang_combo_box") # Settings - self._settings = settings + self._ext_settings = settings + self._settings = Settings(settings.settings) self._profiles = self._settings.profiles self._s_type = self._settings.setting_type self.set_settings() @@ -169,7 +175,6 @@ class SettingsDialog: self._settings.reset() self.set_settings() - @run_idle def set_settings(self): self._host_field.set_text(self._settings.host) self._port_field.set_text(self._settings.port) @@ -207,10 +212,7 @@ class SettingsDialog: self._new_color_button.set_rgba(new_rgb) self._extra_color_button.set_rgba(extra_rgb) - def apply_settings(self, item=None): - if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL: - return - + def on_apply_profile_settings(self, item): self._s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP self._settings.setting_type = self._s_type self._settings.host = self._host_field.get_text() @@ -232,22 +234,28 @@ class SettingsDialog: self._settings.data_local_path = self._data_dir_field.get_text() self._settings.picons_local_path = self._picons_dir_field.get_text() self._settings.backup_local_path = self._backup_dir_field.get_text() - self._settings.backup_before_save = self._before_save_switch.get_active() - self._settings.backup_before_downloading = self._before_downloading_switch.get_active() - self._settings.fav_click_mode = self.get_fav_click_mode() - self._settings.language = self._lang_combo_box.get_active_id() + + def apply_settings(self, item=None): + if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL: + return + + self._ext_settings.profiles = self._settings.profiles + self._ext_settings.backup_before_save = self._before_save_switch.get_active() + self._ext_settings.backup_before_downloading = self._before_downloading_switch.get_active() + self._ext_settings.fav_click_mode = self.get_fav_click_mode() + self._ext_settings.language = self._lang_combo_box.get_active_id() if self._s_type is SettingsType.ENIGMA_2: - self._settings.use_colors = self._set_color_switch.get_active() - self._settings.new_color = self._new_color_button.get_rgba().to_string() - self._settings.extra_color = self._extra_color_button.get_rgba().to_string() - self._settings.v5_support = self._support_ver5_switch.get_active() - self._settings.http_api_support = self._support_http_api_switch.get_active() - self._settings.enable_yt_dl = self._enable_y_dl_switch.get_active() - self._settings.enable_send_to = self._enable_send_to_switch.get_active() + self._ext_settings.use_colors = self._set_color_switch.get_active() + self._ext_settings.new_color = self._new_color_button.get_rgba().to_string() + self._ext_settings.extra_color = self._extra_color_button.get_rgba().to_string() + self._ext_settings.v5_support = self._support_ver5_switch.get_active() + self._ext_settings.http_api_support = self._support_http_api_switch.get_active() + self._ext_settings.enable_yt_dl = self._enable_y_dl_switch.get_active() + self._ext_settings.enable_send_to = self._enable_send_to_switch.get_active() - self._settings.default_profile = list(filter(lambda r: r[1], self._profile_view.get_model()))[0][0] - self._settings.save() + self._ext_settings.default_profile = list(filter(lambda r: r[1], self._profile_view.get_model()))[0][0] + self._ext_settings.save() return True @run_task @@ -342,8 +350,9 @@ class SettingsDialog: self._settings.picons_local_path += p self._settings.backup_local_path += p - def on_profile_edit(self, item): - self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING) + def on_profile_edit(self, item=None): + model, paths = self._profile_view.get_selection().get_selected_rows() + self._profile_view.set_cursor(paths, self._profile_view.get_column(0), True) def on_profile_remove(self, item): model, paths = self._profile_view.get_selection().get_selected_rows() @@ -366,33 +375,30 @@ class SettingsDialog: row = self._profile_view.get_model()[path] row[0] = new_value self._profiles[new_value] = p_name - if p_name == self._settings.current_profile: - self._settings.current_profile = new_value - if p_name == self._settings.default_profile: - self._settings.default_profile = new_value if p_name != new_value: self.update_local_paths(new_value) self.on_profile_selected(self._profile_view) - def update_local_paths(self, p_name): + def update_local_paths(self, p_name, force_rename=False): data_path = self._settings.data_local_path picons_path = self._settings.picons_local_path backup_path = self._settings.backup_local_path - try: - if os.path.isdir(picons_path): - os.rename(picons_path, self._settings.picons_local_path) - if os.path.isdir(data_path): - os.rename(data_path, self._settings.data_local_path) - if os.path.isdir(backup_path): - os.rename(backup_path, self._settings.backup_local_path) - except OSError as e: - self.show_info_message(str(e), Gtk.MessageType.ERROR) - else: - self._settings.data_local_path = "{}/{}/".format(Path(data_path).parent, p_name) - self._settings.picons_local_path = "{}/{}/".format(Path(picons_path).parent, p_name) - self._settings.backup_local_path = "{}/{}/".format(Path(self._settings.backup_local_path).parent, p_name) + self._settings.data_local_path = "{}/{}/".format(Path(data_path).parent, p_name) + self._settings.picons_local_path = "{}/{}/".format(Path(picons_path).parent, p_name) + self._settings.backup_local_path = "{}/{}/".format(Path(backup_path).parent, p_name) + + if force_rename: + try: + if os.path.isdir(picons_path): + os.rename(picons_path, self._settings.picons_local_path) + if os.path.isdir(data_path): + os.rename(data_path, self._settings.data_local_path) + if os.path.isdir(backup_path): + os.rename(backup_path, self._settings.backup_local_path) + except OSError as e: + self.show_info_message(str(e), Gtk.MessageType.ERROR) def on_profile_selected(self, view): model, paths = self._profile_view.get_selection().get_selected_rows() @@ -416,7 +422,10 @@ class SettingsDialog: if box.get_active_id() != self._settings.language: self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING) - def on_current_settings_visible(self, stack: Gtk.Stack, param): + def on_main_settings_visible(self, stack, param): + self._apply_profile_button.set_visible(stack.get_visible_child_name() == "profiles") + + def on_network_settings_visible(self, stack, param): self._http_use_ssl_check_button.set_visible(Property(stack.get_visible_child_name()) is Property.HTTP) def on_http_use_ssl_toggled(self, button): @@ -444,6 +453,10 @@ class SettingsDialog: return FavClickMode.DISABLED + def on_view_popup_menu(self, menu, event): + if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY: + menu.popup(None, None, None, None, event.button, event.time) + if __name__ == "__main__": pass From 024d48b46459b1c476bd1eecb72a0375eb05ae8c Mon Sep 17 00:00:00 2001 From: DYefremov Date: Wed, 8 Jan 2020 14:50:04 +0300 Subject: [PATCH 09/21] set settings type to the profile --- app/settings.py | 4 +- app/ui/settings_dialog.glade | 158 +++++++++++++++-------------------- app/ui/settings_dialog.py | 4 + 3 files changed, 72 insertions(+), 94 deletions(-) diff --git a/app/settings.py b/app/settings.py index ccbadb1f..28c1c78b 100644 --- a/app/settings.py +++ b/app/settings.py @@ -186,9 +186,7 @@ class Settings: @setting_type.setter def setting_type(self, s_type): - self._cp_settings["setting_type"] = s_type - for k, v in s_type.get_default_settings().items(): - self._cp_settings[k] = v + self._cp_settings["setting_type"] = s_type.value @property def language(self): diff --git a/app/ui/settings_dialog.glade b/app/ui/settings_dialog.glade index e8942154..61c21fc2 100644 --- a/app/ui/settings_dialog.glade +++ b/app/ui/settings_dialog.glade @@ -466,6 +466,70 @@ Author: Dmitriy Yefremov True False vertical + + + True + False + 5 + 5 + 5 + 0.019999999552965164 + in + + + True + False + center + center + 5 + 5 + + + Enigma2 + True + True + False + True + neutrino_radio_button + + + + False + True + 1 + + + + + Neutrino-MP + True + True + False + True + enigma_radio_button + + + False + True + 4 + + + + + + + True + False + Settings type: + + + + + False + True + 0 + + True @@ -960,7 +1024,7 @@ Author: Dmitriy Yefremov True True - 0 + 1 @@ -1087,7 +1151,7 @@ Author: Dmitriy Yefremov True True - 1 + 2 @@ -1206,7 +1270,7 @@ Author: Dmitriy Yefremov True True - 2 + 3 @@ -1292,94 +1356,6 @@ Author: Dmitriy Yefremov 0 - - - True - False - 5 - 5 - 5 - 5 - 0.019999999552965164 - in - - - True - False - 5 - 5 - 5 - 5 - - - True - False - Settings type: - - - False - True - 0 - - - - - True - False - center - center - 5 - - - Enigma2 - True - True - False - True - neutrino_radio_button - - - - False - True - 1 - - - - - Neutrino-MP - True - True - False - True - enigma_radio_button - - - False - True - 4 - - - - - False - True - end - 2 - - - - - - - - - - False - True - 1 - - True diff --git a/app/ui/settings_dialog.py b/app/ui/settings_dialog.py index 996056ad..3d5d1fc7 100644 --- a/app/ui/settings_dialog.py +++ b/app/ui/settings_dialog.py @@ -405,6 +405,10 @@ class SettingsDialog: if paths: profile = model.get_value(model.get_iter(paths), 0) self._settings.current_profile = profile + if self._settings.setting_type is SettingsType.ENIGMA_2: + self._enigma_radio_button.activate() + else: + self._neutrino_radio_button.activate() self.set_settings() def on_profile_set_default(self, item): From 2e3ec1c99d3db7d3df0893a148d17d82a66b7131 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Wed, 8 Jan 2020 21:33:24 +0300 Subject: [PATCH 10/21] http api refactoring --- app/connections.py | 82 +++++++++++++++++++------------------- app/ui/main_app_window.py | 83 +++++++++++++++++++-------------------- app/ui/transmitter.py | 2 +- 3 files changed, 83 insertions(+), 84 deletions(-) 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): From 9082d5c96e79983c577ea5fd086b9ecbc00d5925 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Thu, 9 Jan 2020 13:14:49 +0300 Subject: [PATCH 11/21] added last configuration load feature --- app/settings.py | 8 +++++ app/ui/main_app_window.py | 50 +++++++++++++++++++++++------- app/ui/settings_dialog.glade | 60 +++++++++++++++++++++++++++++++++++- app/ui/settings_dialog.py | 3 ++ 4 files changed, 109 insertions(+), 12 deletions(-) diff --git a/app/settings.py b/app/settings.py index 28c1c78b..431e12c0 100644 --- a/app/settings.py +++ b/app/settings.py @@ -196,6 +196,14 @@ class Settings: def language(self, value): self._settings["language"] = value + @property + def load_last_config(self): + return self._settings.get("load_last_config", False) + + @load_last_config.setter + def load_last_config(self, value): + self._settings["load_last_config"] = value + @property def host(self): return self._cp_settings.get("host", self.get_default("host")) diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index f58c9cc4..a15beba8 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -290,9 +290,15 @@ class Application(Gtk.Application): def do_startup(self): Gtk.Application.do_startup(self) - self.init_profiles() self.init_drag_and_drop() self.init_colors() + if self._settings.load_last_config: + config = self._settings.get("last_config") or {} + self.init_profiles(config.get("last_profile", None)) + last_bouquet = config.get("last_bouquet", None) + self.open_data(callback=lambda: self.open_bouquet(last_bouquet)) + else: + self.init_profiles() gen = self.init_http_api() GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) @@ -303,7 +309,11 @@ class Application(Gtk.Application): def do_shutdown(self): """ Performs shutdown tasks """ - self._settings.save() # storing current config + if self._settings.load_last_config: + self._settings.add("last_config", {"last_profile": self._settings.current_profile, + "last_bouquet": self._current_bq_name}) + self._settings.save() # storing current settings + if self._player: self._player.release() Gtk.Application.do_shutdown(self) @@ -318,9 +328,11 @@ class Application(Gtk.Application): self.activate() return 0 - def init_profiles(self): + def init_profiles(self, profile=None): self.update_profiles() - self._profile_combo_box.set_active_id(self._settings.default_profile) + self._profile_combo_box.set_active_id(profile if profile else self._settings.default_profile) + if profile: + self.set_profile(profile) def init_drag_and_drop(self): """ Enable drag-and-drop """ @@ -855,12 +867,12 @@ class Application(Gtk.Application): return self.open_data(response) - def open_data(self, data_path=None): + def open_data(self, data_path=None, callback=None): """ Opening data and fill views. """ - gen = self.update_data(data_path) + gen = self.update_data(data_path, callback) GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_DEFAULT_IDLE) - def update_data(self, data_path): + def update_data(self, data_path, callback=None): self._wait_dialog.show() yield True @@ -892,6 +904,8 @@ class Application(Gtk.Application): yield from self.append_data(bouquets, services) finally: self._wait_dialog.hide() + if callback: + callback() yield True def append_data(self, bouquets, services): @@ -956,6 +970,17 @@ class Application(Gtk.Application): if extra_services: self._extra_bouquets[bq_id] = extra_services + @run_idle + def open_bouquet(self, name): + """ Find and open bouquet by name """ + for r in self._bouquets_model: + for i in r.iterchildren(): + if i[Column.BQ_NAME] == name: + self._bouquets_view.expand_row(self._bouquets_model.get_path(r.iter), Column.BQ_NAME) + self._bouquets_view.set_cursor(i.path) + self._bouquets_view.row_activated(i.path, self._bouquets_view.get_column(Column.BQ_NAME)) + break + def append_services(self, services): for srv in services: # adding channels to dict with fav_id as keys @@ -1202,14 +1227,17 @@ class Application(Gtk.Application): active = self._profile_combo_box.get_active_text() if active in self._settings.profiles: - self._settings.current_profile = active - 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() + self.set_profile(active) gen = self.init_http_api() GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) self.open_data() + def set_profile(self, active): + self._settings.current_profile = active + 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() + def update_profiles(self): self._profile_combo_box.remove_all() for p in self._settings.profiles: diff --git a/app/ui/settings_dialog.glade b/app/ui/settings_dialog.glade index 61c21fc2..166696c9 100644 --- a/app/ui/settings_dialog.glade +++ b/app/ui/settings_dialog.glade @@ -1291,7 +1291,7 @@ Author: Dmitriy Yefremov False vertical - + True False 5 @@ -1356,6 +1356,64 @@ Author: Dmitriy Yefremov 0 + + + True + False + 5 + 5 + 5 + 5 + 0 + in + + + True + False + 5 + 5 + 5 + 5 + + + True + False + start + 5 + 5 + Load the last open configuration at program startup + + + True + True + 0 + + + + + True + True + center + + + False + True + end + 1 + + + + + + + + + + False + True + 1 + + True diff --git a/app/ui/settings_dialog.py b/app/ui/settings_dialog.py index 3d5d1fc7..03e25a56 100644 --- a/app/ui/settings_dialog.py +++ b/app/ui/settings_dialog.py @@ -95,6 +95,7 @@ class SettingsDialog: self._set_color_switch = builder.get_object("set_color_switch") self._new_color_button = builder.get_object("new_color_button") self._extra_color_button = builder.get_object("extra_color_button") + self._load_on_startup_switch = builder.get_object("load_on_startup_switch") # HTTP API self._support_http_api_switch = builder.get_object("support_http_api_switch") self._enable_y_dl_switch = builder.get_object("enable_y_dl_switch") @@ -198,6 +199,7 @@ class SettingsDialog: self._before_save_switch.set_active(self._settings.backup_before_save) self._before_downloading_switch.set_active(self._settings.backup_before_downloading) self.set_fav_click_mode(self._settings.fav_click_mode) + self._load_on_startup_switch.set_active(self._settings.load_last_config) if self._s_type is SettingsType.ENIGMA_2: self._support_ver5_switch.set_active(self._settings.v5_support) @@ -244,6 +246,7 @@ class SettingsDialog: self._ext_settings.backup_before_downloading = self._before_downloading_switch.get_active() self._ext_settings.fav_click_mode = self.get_fav_click_mode() self._ext_settings.language = self._lang_combo_box.get_active_id() + self._ext_settings.load_last_config = self._load_on_startup_switch.get_active() if self._s_type is SettingsType.ENIGMA_2: self._ext_settings.use_colors = self._set_color_switch.get_active() From dfbf019c64b8db4b0484a3367714369b5718f7a3 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Sat, 11 Jan 2020 17:58:50 +0300 Subject: [PATCH 12/21] added token security support --- app/connections.py | 101 +++++++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 40 deletions(-) diff --git a/app/connections.py b/app/connections.py index 172e5f45..ea2be340 100644 --- a/app/connections.py +++ b/app/connections.py @@ -24,8 +24,6 @@ _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 @@ -40,10 +38,11 @@ class HttpRequestType(Enum): ZAP = "zap?sRef=" INFO = "about" SIGNAL = "tunersignal" - STREAM = "streamcurrentm3u" + STREAM = "streamcurrent.m3u" CURRENT = "getcurrent" PLAY = "mediaplayerplay?file=4097:0:1:0:0:0:0:0:0:0:" TEST = None + TOKEN = "session" class TestException(Exception): @@ -106,12 +105,13 @@ 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{}://{}:{}/web/".format("s" if settings.http_use_ssl else "", host, settings.http_port) + base_url = "http{}://{}:{}".format("s" if settings.http_use_ssl else "", host, settings.http_port) + url = "{}/web/".format(base_url) tn, ht = None, None # telnet, http try: if s_type is SettingsType.ENIGMA_2 and use_http: - ht = http(settings.http_user, settings.http_password, base_url, callback) + ht = http(settings.http_user, settings.http_password, base_url, callback, settings.http_use_ssl) next(ht) message = "" if download_type is DownloadType.BOUQUETS: @@ -122,12 +122,11 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False message = "Satellites.xml file will be updated!" params = urlencode({"text": message, "type": 2, "timeout": 5}) - url = base_url + "message?{}".format(params) - ht.send(url) + ht.send((url + "message?{}".format(params), "Sending info message... ")) if download_type is DownloadType.ALL: time.sleep(5) - ht.send(base_url + "powerstate?newstate=0") + ht.send((url + "powerstate?newstate=0", "Toggle Standby ")) time.sleep(2) else: # telnet @@ -172,10 +171,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((url + "servicelistreload?mode=2", "Reloading Userbouquets.")) elif download_type is DownloadType.ALL: - ht.send(base_url + "servicelistreload?mode=0") - ht.send(base_url + "powerstate?newstate=4") + ht.send((url + "servicelistreload?mode=0", "Reloading lamedb and Userbouquets.")) + ht.send((url + "powerstate?newstate=4", "Wakeup from Standby.")) if done_callback is not None: done_callback() @@ -247,13 +246,14 @@ def send_file(file_name, path, ftp, callback): callback("Uploading file: {}. Status: {}\n".format(file_name, str(ftp.storbinary("STOR " + file_name, f)))) -def http(user, password, url, callback): - init_auth(user, password, url) +def http(user, password, url, callback, use_ssl=False): + init_auth(user, password, url, use_ssl) + data = get_post_data(url, password, url) + while True: - url = yield - msg = get_response(HttpRequestType.TEST, url).get("e2statetext", None) - if msg: - callback("HTTP: {}\n".format(msg)) + url, message = yield + resp = get_response(HttpRequestType.TEST, url, data).get("e2statetext", None) + callback("HTTP: {} {}\n".format(message, "Successful." if resp and message else "")) def telnet(host, port=23, user="", password="", timeout=5): @@ -289,6 +289,8 @@ class HttpAPI: def __init__(self, settings): self._settings = settings self._base_url = None + self._session_id = 0 + self._data = None self.init() from concurrent.futures import ThreadPoolExecutor as PoolExecutor @@ -302,22 +304,27 @@ class HttpAPI: elif req_type is HttpRequestType.PLAY: url += urllib.parse.quote(ref).replace("%3A", "%253A") - future = self._executor.submit(get_response, req_type, url) + future = self._executor.submit(get_response, req_type, url, self._data) future.add_done_callback(lambda f: callback(f.result())) def init(self): + user, password = self._settings.http_user, self._settings.http_password 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 = "{}/web/".format(url) - init_auth(self._settings.http_user, self._settings.http_password, url, use_ssl) + init_auth(user, password, url, use_ssl) + url = "{}/web/{}".format(url, HttpRequestType.TOKEN.value) + s_id = get_session_id(user, password, url) + if s_id != "0": + self._data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8") def close(self): self._executor.shutdown(False) -def get_response(req_type, url): +def get_response(req_type, url, data=None): try: - with urlopen(Request(url, headers=_HEADERS), timeout=10) as f: + with urlopen(Request(url, data=data), timeout=10) as f: if req_type is HttpRequestType.STREAM: return f.read().decode("utf-8") elif req_type is HttpRequestType.CURRENT: @@ -334,6 +341,36 @@ def get_response(req_type, url): return {} +def init_auth(user, password, url, use_ssl=False): + """ Init authentication """ + pass_mgr = HTTPPasswordMgrWithDefaultRealm() + pass_mgr.add_password(None, url, user, password) + auth_handler = HTTPBasicAuthHandler(pass_mgr) + + if use_ssl: + import ssl + from urllib.request import HTTPSHandler + + opener = build_opener(auth_handler, HTTPSHandler(context=ssl._create_unverified_context())) + else: + opener = build_opener(auth_handler) + + install_opener(opener) + + +def get_session_id(user, password, url): + data = urllib.parse.urlencode(dict(user=user, password=password)).encode("utf-8") + return get_response(HttpRequestType.TOKEN, url, data=data).get("e2sessionid", "0") + + +def get_post_data(base_url, password, user): + s_id = get_session_id(user, password, "{}/web/{}".format(base_url, HttpRequestType.TOKEN.value)) + data = None + if s_id != "0": + data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8") + return data + + # ***************** Connections testing *******************# def test_ftp(host, port, user, password, timeout=5): @@ -350,30 +387,14 @@ def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message base_url = "http{}://{}:{}".format("s" if use_ssl else "", host, port) # authentication init_auth(user, password, base_url, use_ssl) + data = get_post_data(base_url, password, user) + try: - return get_response(HttpRequestType.TEST, "{}/web/{}".format(base_url, params)).get("e2statetext", "") + return get_response(HttpRequestType.TEST, "{}/web/{}".format(base_url, params), data).get("e2statetext", "") except (RemoteDisconnected, URLError, HTTPError) as e: raise TestException(e) -def init_auth(user, password, url, use_ssl=False): - """ Init authentication """ - pass_mgr = HTTPPasswordMgrWithDefaultRealm() - pass_mgr.add_password(None, url, user, password) - auth_handler = HTTPBasicAuthHandler(pass_mgr) - - if use_ssl: - import ssl - from urllib.request import HTTPSHandler - # https://stackoverflow.com/a/28052583 - context = ssl._create_unverified_context() - opener = build_opener(auth_handler, HTTPSHandler(context=context)) - else: - opener = build_opener(auth_handler) - - install_opener(opener) - - def test_telnet(host, port, user, password, timeout=5): try: gen = telnet_test(host, port, user, password, timeout) From 47c80b2b2964e4e22102884a3865771e01b9d93d Mon Sep 17 00:00:00 2001 From: DYefremov Date: Sat, 11 Jan 2020 20:40:16 +0300 Subject: [PATCH 13/21] minor fix of status update --- app/ui/main_app_window.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index a15beba8..24280b46 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -1803,7 +1803,7 @@ class Application(Gtk.Application): 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) + GLib.idle_add(self._signal_level_bar.set_visible, has_onid) def update_signal(self, sig): self.set_signal(sig.get("e2snr", "0 %") if sig else "0 %") @@ -1814,12 +1814,14 @@ class Application(Gtk.Application): def update_status(self, evn): if evn: - s_duration = int(evn.get("e2eventstart", "0")) + s_duration = evn.get("e2eventstart", 0) + if not s_duration: + return + s_duration = int(s_duration) 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) + end_time = datetime.fromtimestamp(s_duration + int(evn.get("e2eventduration", "0") or "0")) + title = evn.get("e2eventtitle", "") + dsc = "{} {}:{} - {}:{}".format(title, 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(evn.get("e2eventdescription", "")) From a1098750da350d9e95a2a49696086fb5884b2a69 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Sun, 12 Jan 2020 00:33:33 +0300 Subject: [PATCH 14/21] fix data loading if pixbuf error --- app/ui/main_app_window.py | 3 +++ app/ui/main_helper.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index 24280b46..461d4e00 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -873,6 +873,7 @@ class Application(Gtk.Application): GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_DEFAULT_IDLE) def update_data(self, data_path, callback=None): + self._profile_combo_box.set_sensitive(False) self._wait_dialog.show() yield True @@ -904,6 +905,7 @@ class Application(Gtk.Application): yield from self.append_data(bouquets, services) finally: self._wait_dialog.hide() + self._profile_combo_box.set_sensitive(True) if callback: callback() yield True @@ -1019,6 +1021,7 @@ class Application(Gtk.Application): self._blacklist.clear() self._services.clear() self._rows_buffer.clear() + self._picons.clear() self._bouquets.clear() self._extra_bouquets.clear() self._current_bq_name = None diff --git a/app/ui/main_helper.py b/app/ui/main_helper.py index 39226f8d..a7ca724e 100644 --- a/app/ui/main_helper.py +++ b/app/ui/main_helper.py @@ -10,8 +10,8 @@ from app.eparser import Service from app.eparser.ecommons import Flag, BouquetService, Bouquet, BqType from app.eparser.enigma.bouquets import BqServiceType, to_bouquet_id from app.settings import SettingsType -from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog +from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column # ***************** Markers *******************# @@ -347,7 +347,9 @@ def update_picons_data(path, picons): return for file in os.listdir(path): - picons[file] = get_picon_pixbuf(path + file) + pf = get_picon_pixbuf(path + file) + if pf: + picons[file] = pf def append_picons(picons, model): @@ -487,7 +489,10 @@ def is_only_one_item_selected(paths, transient): def get_picon_pixbuf(path): - return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=path, width=32, height=32, preserve_aspect_ratio=True) + try: + return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=path, width=32, height=32, preserve_aspect_ratio=True) + except GLib.GError as e: + pass # ***************** Bouquets *********************# From 0f6423431262fb38ca9ed4bb05cb150015d9ed9b Mon Sep 17 00:00:00 2001 From: DYefremov Date: Tue, 14 Jan 2020 18:26:05 +0300 Subject: [PATCH 15/21] added stream mode --- app/connections.py | 7 +-- app/ui/main_app_window.py | 74 +++++++++++++++++++++++-------- app/ui/settings_dialog.glade | 84 ++++++++++++++++++++++-------------- app/ui/settings_dialog.py | 26 +++++++++-- app/ui/uicommons.py | 5 ++- 5 files changed, 138 insertions(+), 58 deletions(-) diff --git a/app/connections.py b/app/connections.py index ea2be340..8d6cdc84 100644 --- a/app/connections.py +++ b/app/connections.py @@ -38,7 +38,8 @@ class HttpRequestType(Enum): ZAP = "zap?sRef=" INFO = "about" SIGNAL = "tunersignal" - STREAM = "streamcurrent.m3u" + STREAM = "stream.m3u?ref=" + STREAM_CURRENT = "streamcurrent.m3u" CURRENT = "getcurrent" PLAY = "mediaplayerplay?file=4097:0:1:0:0:0:0:0:0:0:" TEST = None @@ -299,7 +300,7 @@ class HttpAPI: def send(self, req_type, ref, callback=print): url = self._base_url + req_type.value - if req_type is HttpRequestType.ZAP: + if req_type is HttpRequestType.ZAP or req_type is HttpRequestType.STREAM: url += urllib.parse.quote(ref) elif req_type is HttpRequestType.PLAY: url += urllib.parse.quote(ref).replace("%3A", "%253A") @@ -325,7 +326,7 @@ class HttpAPI: def get_response(req_type, url, data=None): try: with urlopen(Request(url, data=data), timeout=10) as f: - if req_type is HttpRequestType.STREAM: + if req_type is HttpRequestType.STREAM or req_type is HttpRequestType.STREAM_CURRENT: return f.read().decode("utf-8") elif req_type is HttpRequestType.CURRENT: for e in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"): diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index 461d4e00..ddc9866c 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -1430,10 +1430,12 @@ class Application(Gtk.Application): return elif self._fav_click_mode is FavClickMode.STREAM: self.on_play_stream() - elif self._fav_click_mode is FavClickMode.PLAY: + elif self._fav_click_mode is FavClickMode.ZAP_PLAY: self.on_zap(self.on_watch) elif self._fav_click_mode is FavClickMode.ZAP: self.on_zap() + elif self._fav_click_mode is FavClickMode.PLAY: + self.on_stream() else: return self.on_view_popup_menu(menu, event) @@ -1632,11 +1634,19 @@ 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_zap(self.on_watch) if self._fav_click_mode is FavClickMode.PLAY else self.on_play_stream() + self.set_player_action() def on_player_next(self, item): if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1): - self.on_zap(self.on_watch) if self._fav_click_mode is FavClickMode.PLAY else self.on_play_stream() + self.set_player_action() + + def set_player_action(self): + if self._fav_click_mode is FavClickMode.PLAY: + self.on_stream() + elif self._fav_click_mode is FavClickMode.ZAP_PLAY: + self.on_zap(self.on_watch) + elif self._fav_click_mode is FavClickMode.STREAM: + self.on_play_stream() def on_player_rewind(self, scale, scroll_type, value): self._player.set_time(int(value)) @@ -1745,9 +1755,23 @@ class Application(Gtk.Application): elif self._links_transmitter: self._links_transmitter.show(enable) + def on_stream(self, item=None): + path, column = self._fav_view.get_cursor() + if not path or not self._http_api: + return + + ref = self.get_service_ref(path) + if not ref: + return + + if self._player and self._player.is_playing(): + self._player.stop() + + self._http_api.send(HttpRequestType.STREAM, ref, self.watch) + def on_watch(self, item=None): """ Switch to the channel and watch in the player """ - self._http_api.send(HttpRequestType.STREAM, None, self.watch) + self._http_api.send(HttpRequestType.STREAM_CURRENT, None, self.watch) def watch(self, m3u): if m3u: @@ -1762,21 +1786,32 @@ class Application(Gtk.Application): if not path or not self._http_api: return + ref = self.get_service_ref(path) + if not ref: + return + if self._player and self._player.is_playing(): self._player.stop() + def zap(rq): + if rq and rq.get("e2state", False): + GLib.idle_add(scroll_to, path, self._fav_view) + if callback: + callback() + + self._http_api.send(HttpRequestType.ZAP, ref, zap) + + def get_service_ref(self, path): row = self._fav_model[path][:] - srv = self._services.get(row[Column.FAV_ID], None) + srv_type, fav_id = row[Column.FAV_TYPE], row[Column.FAV_ID] + + if srv_type == BqServiceType.IPTV.name or srv_type == BqServiceType.MARKER.name: + self.show_error_dialog("Not allowed in this context!") + return + + srv = self._services.get(fav_id, None) if srv and srv.transponder: - ref = srv.picon_id.rstrip(".png").replace("_", ":") - - def zap(rq): - if rq and rq.get("e2state", False): - GLib.idle_add(scroll_to, path, self._fav_view) - if callback is not None: - callback() - - self._http_api.send(HttpRequestType.ZAP, ref, zap) + return srv.picon_id.rstrip(".png").replace("_", ":") def update_info(self): """ Updating current info over HTTP API """ @@ -1795,10 +1830,13 @@ class Application(Gtk.Application): 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)) + GLib.idle_add(self._receiver_info_label.set_text, info_text, priority=GLib.PRIORITY_LOW) + service_name = info.get("e2servicename", None) or "" + GLib.idle_add(self._service_name_label.set_text, service_name, priority=GLib.PRIORITY_LOW) + GLib.idle_add(self._signal_box.set_visible, bool(service_name), priority=GLib.PRIORITY_LOW) + if service_name: + self.update_service_info(info) + GLib.idle_add(self._receiver_info_box.set_visible, bool(res_info), priority=GLib.PRIORITY_LOW) @run_idle def update_service_info(self, info): diff --git a/app/ui/settings_dialog.glade b/app/ui/settings_dialog.glade index 166696c9..0a7aaf05 100644 --- a/app/ui/settings_dialog.glade +++ b/app/ui/settings_dialog.glade @@ -1874,41 +1874,10 @@ Author: Dmitriy Yefremov False Switch(zap) the channel(Ctrl + Z) True - click_mode_disabled_button - - - 0 - 0 - - - - - Play - True - False - True - False - Switch the channel and watch in the program(Ctrl + W) - True - click_mode_stream_button - - - 1 - 0 - - - - - Play stream - True - True - False - Play IPTV or other stream in the program(Ctrl + P) - True click_mode_play_button - 2 + 0 0 @@ -1920,13 +1889,62 @@ Author: Dmitriy Yefremov False Disabled True - click_mode_play_button + click_mode_zap_button + + + 4 + 0 + + + + + Play stream + True + True + False + Play IPTV or other stream in the program(Ctrl + P) + True + click_mode_disabled_button 3 0 + + + Zap and Play + True + False + True + False + Switch the channel and watch in the program(Ctrl + W) + True + click_mode_stream_button + + + 2 + 0 + + + + + Play + True + False + True + False + Watch the channel in the program + True + True + click_mode_zap_and_play_button + + + + 1 + 0 + + diff --git a/app/ui/settings_dialog.py b/app/ui/settings_dialog.py index 03e25a56..00a02485 100644 --- a/app/ui/settings_dialog.py +++ b/app/ui/settings_dialog.py @@ -47,6 +47,7 @@ class SettingsDialog: "on_main_settings_visible": self.on_main_settings_visible, "on_network_settings_visible": self.on_network_settings_visible, "on_http_use_ssl_toggled": self.on_http_use_ssl_toggled, + "on_click_mode_togged": self.on_click_mode_togged, "on_view_popup_menu": self.on_view_popup_menu} builder = Gtk.Builder() @@ -56,6 +57,7 @@ class SettingsDialog: self._dialog = builder.get_object("settings_dialog") self._dialog.set_transient_for(transient) self._header_bar = builder.get_object("header_bar") + self._main_stack = builder.get_object("main_stack") # Network self._host_field = builder.get_object("host_field") self._port_field = builder.get_object("port_field") @@ -104,7 +106,9 @@ class SettingsDialog: self._click_mode_stream_button = builder.get_object("click_mode_stream_button") self._click_mode_play_button = builder.get_object("click_mode_play_button") self._click_mode_zap_button = builder.get_object("click_mode_zap_button") + self._click_mode_zap_and_play_button = builder.get_object("click_mode_zap_and_play_button") self._click_mode_zap_button.bind_property("sensitive", self._click_mode_play_button, "sensitive") + self._click_mode_zap_button.bind_property("sensitive", self._click_mode_zap_and_play_button, "sensitive") self._click_mode_zap_button.bind_property("sensitive", self._enable_send_to_switch, "sensitive") self._enable_send_to_switch.bind_property("sensitive", builder.get_object("enable_send_to_label"), "sensitive") self._extra_support_grid.bind_property("sensitive", builder.get_object("v5_support_grid"), "sensitive") @@ -169,10 +173,10 @@ class SettingsDialog: profile = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP self._s_type = profile self._settings.setting_type = profile - self.set_settings() + self.on_reset() self.init_ui_elements(profile) - def on_reset(self, item): + def on_reset(self, item=None): self._settings.reset() self.set_settings() @@ -327,7 +331,9 @@ class SettingsDialog: def on_http_mode_switch_state(self, switch, state): self._click_mode_zap_button.set_sensitive(state) - if self._click_mode_play_button.get_active() or self._click_mode_zap_button.get_active(): + if any((self._click_mode_play_button.get_active(), + self._click_mode_zap_button.get_active(), + self._click_mode_zap_and_play_button.get_active())): self._click_mode_disabled_button.set_active(True) def on_yt_dl_switch_state(self, switch, state): @@ -352,6 +358,7 @@ class SettingsDialog: self._settings.data_local_path += p self._settings.picons_local_path += p self._settings.backup_local_path += p + self.on_reset() def on_profile_edit(self, item=None): model, paths = self._profile_view.get_selection().get_selected_rows() @@ -442,6 +449,16 @@ class SettingsDialog: self._http_port_field.set_text(port) self._settings.http_port = port + def on_click_mode_togged(self, button): + if self._main_stack.get_visible_child_name() != "extra": + return + + mode = self.get_fav_click_mode() + if mode is FavClickMode.PLAY: + self.show_info_message("Operates in standby mode or current active transponder!", Gtk.MessageType.WARNING) + else: + self.on_info_bar_close() + @run_idle def set_fav_click_mode(self, mode): mode = FavClickMode(mode) @@ -449,12 +466,15 @@ class SettingsDialog: self._click_mode_stream_button.set_active(mode is FavClickMode.STREAM) self._click_mode_play_button.set_active(mode is FavClickMode.PLAY) self._click_mode_zap_button.set_active(mode is FavClickMode.ZAP) + self._click_mode_zap_and_play_button.set_active(mode is FavClickMode.ZAP_PLAY) def get_fav_click_mode(self): if self._click_mode_zap_button.get_active(): return FavClickMode.ZAP if self._click_mode_play_button.get_active(): return FavClickMode.PLAY + if self._click_mode_zap_and_play_button.get_active(): + return FavClickMode.ZAP_PLAY if self._click_mode_stream_button.get_active(): return FavClickMode.STREAM diff --git a/app/ui/uicommons.py b/app/ui/uicommons.py index 4840e949..5e1ea6ae 100644 --- a/app/ui/uicommons.py +++ b/app/ui/uicommons.py @@ -1,9 +1,11 @@ import locale import os from enum import Enum, IntEnum -from app.settings import Settings, SettingsException import gi + +from app.settings import Settings, SettingsException + gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk @@ -91,6 +93,7 @@ class FavClickMode(IntEnum): STREAM = 1 PLAY = 2 ZAP = 3 + ZAP_PLAY = 4 class ViewTarget(Enum): From cc15594338c0afe19f3f2db944b34602e0dd76fe Mon Sep 17 00:00:00 2001 From: DYefremov Date: Wed, 15 Jan 2020 07:24:16 +0300 Subject: [PATCH 16/21] added app icon --- app/ui/dialogs.glade | 5 +- .../icons/hicolor/96x96/apps/demon-editor.png | Bin 0 -> 4304 bytes .../hicolor/scalable/apps/demon-editor.svg | 634 ++++++++++++++++++ app/ui/main_window.glade | 4 +- app/ui/uicommons.py | 2 + .../share/applications/DemonEditor.desktop | 4 +- .../icons/hicolor/96x96/apps/demon-editor.png | Bin 0 -> 4304 bytes .../hicolor/scalable/apps/demon-editor.svg | 634 ++++++++++++++++++ 8 files changed, 1277 insertions(+), 6 deletions(-) create mode 100644 app/ui/icons/hicolor/96x96/apps/demon-editor.png create mode 100644 app/ui/icons/hicolor/scalable/apps/demon-editor.svg create mode 100644 deb/usr/share/icons/hicolor/96x96/apps/demon-editor.png create mode 100644 deb/usr/share/icons/hicolor/scalable/apps/demon-editor.svg diff --git a/app/ui/dialogs.glade b/app/ui/dialogs.glade index 77d49bd4..ae0737cc 100644 --- a/app/ui/dialogs.glade +++ b/app/ui/dialogs.glade @@ -41,7 +41,7 @@ Author: Dmitriy Yefremov normal DemonEditor 0.4.7 Pre-alpha - 2018-2019 Dmitriy Yefremov + 2018-2020 Dmitriy Yefremov Enigma2 channel and satellites list editor for GNU/Linux https://dyefremov.github.io/DemonEditor/ @@ -50,7 +50,8 @@ Author: Dmitriy Yefremov Dmitriy Yefremov translator-credits - accessories-text-editor + Program logo: <a href="http://ihad.tv"> mfgeg</a> + demon-editor True mit-x11 diff --git a/app/ui/icons/hicolor/96x96/apps/demon-editor.png b/app/ui/icons/hicolor/96x96/apps/demon-editor.png new file mode 100644 index 0000000000000000000000000000000000000000..0c560a60931e3f4bd4b330fbddbc9713fef45e9d GIT binary patch literal 4304 zcmV;>5HIhEP)>l2(#Crc^K1n>laDF7@0JOBa@B~Bj5 z>C>lge*5jW|K#)e(lr_l1>i85OpRZD`Q^ph+S;4)U^6m!EdY)I=zX9iE;BQ8_4e)C zw}_%Rfl>-eDFL8MPEHPmfy=1P;J>01E+31<(h;4uC#r5?B82 zyYK#MPEJm-D2iYh1`NZ1WmynHz%UFrjswfGlGp3Cwzai2ef;sq-__UG+W=_f`EVzI z)(1)A=Dhjln}3=yW5#^9+YN?cKnUpvOnD6egb>1UoFOwaGiUko<#XrFo0sZvI4mZU z$uElHd0B-Y2;fR8Dk}bI&YU?g`P7E15p%_mX?-TT3R~m>eZ{)Z``G697hYIxv)K~VxFMx9rXAn_{7z*n{T%@RU9MSFUS7U_{rdH*IyyQM zhoYYGAaR^dr(2`f>zR-dqv0bMKALwprvQk@<5?j|(jEY9AgwFXhYz1SbLPxVj^jvPUS87r_3Mj!dwZvw&1SHy`YebjL@$POTt5(Z11glo zE7vJ}D)e)^-HsoA_+dg}Vd0vhq9VUc8My1$uQQS)MFq5ek&iWSDvvxwQAA%~A0dQj z!v`Z;E!L_!+yr>UswyK%5+1e(SmI>Qn0Fb1%m&9R|G{4`EZQHh0oI7{!{KbnGe`#rH z$+lXpPdS~=fXCyZgb+|li6lwX?RJazx}PLTRE`vh0CoWAx(&Ed!bm_VB_$;##U&*r za~X!AN+2T4U#7}?5kBibd_JG&$tR!O+uq(D0I(Fme~dcg5Xo<4?%deW7Jja@l$4Z& z*Is+=HwY&C{02bIXmz<%ZK5dRx#ymnuhnX`2@nC1vszQ3#--Yuvjb(i^bx!*=#PS)9G@%-A=F9 z>+<{kPDzr4kz|m@jT@J?Xwjl^2rfMReN>EtAP8Qw*__bV*O%aMICz)KWe^0x;C8!t zpU-Cq1OmJ$ih4pwfam!hp68`#MupW<+1FoxeW7e5bO4H@p{i8A=WPe2wEwjzik#Q$ zO(%rVv17;j($mu=olXZylHyJhB}u}RDN{1b%F1#OTrd7D0A^U}NFWekT`rea6h$T{ zC&!zcn@c$^{y4eM=fjpQTjt4vkq%%?Tm&*DNrJ^<(E-4W88f7@W5>pwx=x!mZG28n zjsd|n;z@BsM+hNsyWPZUwKC(!kH_T6lM%AuJ?t$tH8pw0j2X!YF0jvvOV7b>wB>e|Z0Qrrh4K^BXJ@H~%#f&vWpN*o~sH*emwA3AiX1He82 zr@0tNoQGkU9<5d@@jOrUdOgwUbb5_OV_+C2;ck}Q>2x9|CkJ_XdANT4`mh04S66rK z>8GFm5Wrplj+lg%7>4QOIPO`V=bQ9;JxxeRARNc>gpl0iY1X@hON`w%?vg}>G{ja|Iik6m^wj4fuctuuL)`5(S z3>w}UyWNh-lP5!`(?u0DtJP{REiK)1>C&Yy0Nhc;qt4?r8V$KuYS*%5%TQHSg)LjQ zxKErou^}lb>Aw;Z5^j@Tzu%Aa^z^8K=Jk4gD_5@kw4VsPJ1XmVbWakw+RrIh@=vOqehsD(dQVI$dRDWqX>Nn``bSX)&4O6yP)( z&45S!^UpuW?Afz#n$6~SCQh7qTP|`&Mg};JQ*E%74w z2tgnaxNj{HMG^16|Nc4z*AUhWtHddpKVk0;$*L9=6(K1pX{;a!nc?c|a=BFDafykE zd{tG|0sylKA#;ZfxJW7~^SD6)r^MZSKHn1#hvSy=OpfDVwOUot4oWE&E?ijj{PWLG zqLh{+xb!zXRyADWIF2KPkU@P(Nl6I+K&R7f@pwG9e5TcEVKSLi%S#bO5k{kN?WRqe z9tZFW!!S9+qN&qJ3IT0rlzJ_0Ag7dCX*>@k(?bnc09X% z`}Q&bn^>03yQfuEGemQTc-(+MGa8K`gdjaVeOGgH^Q2J9AxRQ=o=0bAXVlWiBuT=H zFTPl|YuB!Yq9|4Xn0BXYRb8x!#OMcNRcmT$uz2xe0AQ?E>pr{P{;bt%MQBJ#akJDg z4ELJz)~;RqL|$HA*0N>G^nSnpD1sYb-P%_*qp6YHAX!x-UVruKRRF-^#fx#`#0mY4 z8#fO2^z{5gV`F0^rjN^)FC&x;N2>(_0X+KXqtoi^>wiCP+_-W8>tgCTDD)w`naySZ zKz@Ec0FZ0fuKi*5?Ai4_Jv}R%nwoCAhgPdaZ*Ol@)ijdYrlh1K)z#Jge%-ots{#Cx zW!c22sA?qZNeE#$j$?Q4-pwQ@Cu7W*F{HV<8TNACfh5zDmxiU_jI>m4t zciSqK5-vY;=1k1^Ia8YcnxHvm0cGRnj^E_8sS-Ba&7pfbFaQbli^l80TtNpD`r-MeLLFiwdP6w@4dy5eWm$a6a7Bn?A zVK@^Gl|D5#Hd+y^6Dw6UtfW+{M2gXcZ((*X70J!bh2QVT&p-bh7pY!0o6U9n`0*|T zw~qT?)#tc@Ox|}JC`pnqb?Q`fcXy+qp&{a63`c8kZ#TELwh9Oqi?pZ>935&WMNvd@ zax(Js^U>1Mg2u+ixB*S0(O~c1z27OM0DzTKd9h|l9VLX|@y8!WYHBKuA3qMC&lfkD zn1m2G91ickefzE=SR_)XIvtUV%I>MAqA0@e_ai$y8~OS9Fc=JIYimPoZ7nn!&9GU| zJI&h%4jecw8wD?b3(;`7n9!8f&_j|WWlu}YoH-M7=gx)6WWw3AXCVke|E{a?R>tIZ zyZxVh@=2qNRc${3R@3F`>+3^tadAJFQ>cc)V1Q1i1JCmiMG@WI-PpT#FZ_N#bUGd4 zxw@vKqr=?T*yuvABP3 zzu#{|aFZ93ot>>qN=h=Mr>7ed5)uqrzxN@exD4Wm&baU92?ySuyl zv(G*|edy4kMgTnkegJSLX?e|-^Zmj{}Mle(S=a|l^YIzN@QcY15rJXx>9yojUtQo*f0RIla6?0>xOtA4j z`4d9W+S=N@b?er@*=#le!6J?$!)C>m+JShU%e-E%XV0EJ2fz8|n{G*xtN^NHX&XNJ z6R#@CFbrHSm#v|pp<&mqU8fP;_u~?PFA(FM^hX46a{gQ2kt0WH&!0cvVX;^M;Qs)8 zg<<&pt-D!OI$Wx%IMsnbz$XZT{pQV^UB`|cyLjQkg?5=%bjpBr#oYi?N)ZSI`txc- zBLtB)AP8MoPy&|s+NgjK0*>RL(P;W(aCW=h)ZEz5{N^Ff%N+n6^KZjf{pnRtMkcKFVk|>G+ zN+}Hl0)CIj<1?Ghw$9E@+x6?$t!A^?Ej!O%`EUWaBF|yH7y*s2EW1MvUDHU^WB}*k y|0l^4IYDp@5;94(h8z|Im)YF%HJ*`1!2cfxMbJWHiy=S&0000 + + + + + DeamonEditor Icons + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + DeamonEditor Icons + + + mfgeg + + + 7.1.2020 + + + + + + + + + + + + + + + + diff --git a/app/ui/main_window.glade b/app/ui/main_window.glade index 64b45d84..6bc6e2ee 100644 --- a/app/ui/main_window.glade +++ b/app/ui/main_window.glade @@ -964,7 +964,7 @@ Author: Dmitriy Yefremov 480 False center - accessories-text-editor + demon-editor center DemonEditor @@ -2749,7 +2749,7 @@ Author: Dmitriy Yefremov True False - gtk-edit + demon-editor 6 diff --git a/app/ui/uicommons.py b/app/ui/uicommons.py index 5e1ea6ae..31eb91b1 100644 --- a/app/ui/uicommons.py +++ b/app/ui/uicommons.py @@ -25,6 +25,8 @@ else: locale.bindtextdomain(TEXT_DOMAIN, UI_RESOURCES_PATH + "lang") theme = Gtk.IconTheme.get_default() +theme.append_search_path(UI_RESOURCES_PATH + "icons") + _IMAGE_MISSING = theme.load_icon("image-missing", 16, 0) if theme.lookup_icon("image-missing", 16, 0) else None CODED_ICON = theme.load_icon("emblem-readonly", 16, 0) if theme.lookup_icon( "emblem-readonly", 16, 0) else _IMAGE_MISSING diff --git a/deb/usr/share/applications/DemonEditor.desktop b/deb/usr/share/applications/DemonEditor.desktop index 05d277da..2b14d9bd 100755 --- a/deb/usr/share/applications/DemonEditor.desktop +++ b/deb/usr/share/applications/DemonEditor.desktop @@ -3,8 +3,8 @@ Version=1.0 Name=DemonEditor Comment=Channels and satellites list editor for Enigma2 Comment[ru]=Редактор списка каналов и спутников для Enigma2 -Icon=accessories-text-editor -Exec=/usr/bin/demoneditor.sh +Icon=demon-editor +Exec=/usr/bin/demon-editor Terminal=false Type=Application Categories=Utility;Application; diff --git a/deb/usr/share/icons/hicolor/96x96/apps/demon-editor.png b/deb/usr/share/icons/hicolor/96x96/apps/demon-editor.png new file mode 100644 index 0000000000000000000000000000000000000000..0c560a60931e3f4bd4b330fbddbc9713fef45e9d GIT binary patch literal 4304 zcmV;>5HIhEP)>l2(#Crc^K1n>laDF7@0JOBa@B~Bj5 z>C>lge*5jW|K#)e(lr_l1>i85OpRZD`Q^ph+S;4)U^6m!EdY)I=zX9iE;BQ8_4e)C zw}_%Rfl>-eDFL8MPEHPmfy=1P;J>01E+31<(h;4uC#r5?B82 zyYK#MPEJm-D2iYh1`NZ1WmynHz%UFrjswfGlGp3Cwzai2ef;sq-__UG+W=_f`EVzI z)(1)A=Dhjln}3=yW5#^9+YN?cKnUpvOnD6egb>1UoFOwaGiUko<#XrFo0sZvI4mZU z$uElHd0B-Y2;fR8Dk}bI&YU?g`P7E15p%_mX?-TT3R~m>eZ{)Z``G697hYIxv)K~VxFMx9rXAn_{7z*n{T%@RU9MSFUS7U_{rdH*IyyQM zhoYYGAaR^dr(2`f>zR-dqv0bMKALwprvQk@<5?j|(jEY9AgwFXhYz1SbLPxVj^jvPUS87r_3Mj!dwZvw&1SHy`YebjL@$POTt5(Z11glo zE7vJ}D)e)^-HsoA_+dg}Vd0vhq9VUc8My1$uQQS)MFq5ek&iWSDvvxwQAA%~A0dQj z!v`Z;E!L_!+yr>UswyK%5+1e(SmI>Qn0Fb1%m&9R|G{4`EZQHh0oI7{!{KbnGe`#rH z$+lXpPdS~=fXCyZgb+|li6lwX?RJazx}PLTRE`vh0CoWAx(&Ed!bm_VB_$;##U&*r za~X!AN+2T4U#7}?5kBibd_JG&$tR!O+uq(D0I(Fme~dcg5Xo<4?%deW7Jja@l$4Z& z*Is+=HwY&C{02bIXmz<%ZK5dRx#ymnuhnX`2@nC1vszQ3#--Yuvjb(i^bx!*=#PS)9G@%-A=F9 z>+<{kPDzr4kz|m@jT@J?Xwjl^2rfMReN>EtAP8Qw*__bV*O%aMICz)KWe^0x;C8!t zpU-Cq1OmJ$ih4pwfam!hp68`#MupW<+1FoxeW7e5bO4H@p{i8A=WPe2wEwjzik#Q$ zO(%rVv17;j($mu=olXZylHyJhB}u}RDN{1b%F1#OTrd7D0A^U}NFWekT`rea6h$T{ zC&!zcn@c$^{y4eM=fjpQTjt4vkq%%?Tm&*DNrJ^<(E-4W88f7@W5>pwx=x!mZG28n zjsd|n;z@BsM+hNsyWPZUwKC(!kH_T6lM%AuJ?t$tH8pw0j2X!YF0jvvOV7b>wB>e|Z0Qrrh4K^BXJ@H~%#f&vWpN*o~sH*emwA3AiX1He82 zr@0tNoQGkU9<5d@@jOrUdOgwUbb5_OV_+C2;ck}Q>2x9|CkJ_XdANT4`mh04S66rK z>8GFm5Wrplj+lg%7>4QOIPO`V=bQ9;JxxeRARNc>gpl0iY1X@hON`w%?vg}>G{ja|Iik6m^wj4fuctuuL)`5(S z3>w}UyWNh-lP5!`(?u0DtJP{REiK)1>C&Yy0Nhc;qt4?r8V$KuYS*%5%TQHSg)LjQ zxKErou^}lb>Aw;Z5^j@Tzu%Aa^z^8K=Jk4gD_5@kw4VsPJ1XmVbWakw+RrIh@=vOqehsD(dQVI$dRDWqX>Nn``bSX)&4O6yP)( z&45S!^UpuW?Afz#n$6~SCQh7qTP|`&Mg};JQ*E%74w z2tgnaxNj{HMG^16|Nc4z*AUhWtHddpKVk0;$*L9=6(K1pX{;a!nc?c|a=BFDafykE zd{tG|0sylKA#;ZfxJW7~^SD6)r^MZSKHn1#hvSy=OpfDVwOUot4oWE&E?ijj{PWLG zqLh{+xb!zXRyADWIF2KPkU@P(Nl6I+K&R7f@pwG9e5TcEVKSLi%S#bO5k{kN?WRqe z9tZFW!!S9+qN&qJ3IT0rlzJ_0Ag7dCX*>@k(?bnc09X% z`}Q&bn^>03yQfuEGemQTc-(+MGa8K`gdjaVeOGgH^Q2J9AxRQ=o=0bAXVlWiBuT=H zFTPl|YuB!Yq9|4Xn0BXYRb8x!#OMcNRcmT$uz2xe0AQ?E>pr{P{;bt%MQBJ#akJDg z4ELJz)~;RqL|$HA*0N>G^nSnpD1sYb-P%_*qp6YHAX!x-UVruKRRF-^#fx#`#0mY4 z8#fO2^z{5gV`F0^rjN^)FC&x;N2>(_0X+KXqtoi^>wiCP+_-W8>tgCTDD)w`naySZ zKz@Ec0FZ0fuKi*5?Ai4_Jv}R%nwoCAhgPdaZ*Ol@)ijdYrlh1K)z#Jge%-ots{#Cx zW!c22sA?qZNeE#$j$?Q4-pwQ@Cu7W*F{HV<8TNACfh5zDmxiU_jI>m4t zciSqK5-vY;=1k1^Ia8YcnxHvm0cGRnj^E_8sS-Ba&7pfbFaQbli^l80TtNpD`r-MeLLFiwdP6w@4dy5eWm$a6a7Bn?A zVK@^Gl|D5#Hd+y^6Dw6UtfW+{M2gXcZ((*X70J!bh2QVT&p-bh7pY!0o6U9n`0*|T zw~qT?)#tc@Ox|}JC`pnqb?Q`fcXy+qp&{a63`c8kZ#TELwh9Oqi?pZ>935&WMNvd@ zax(Js^U>1Mg2u+ixB*S0(O~c1z27OM0DzTKd9h|l9VLX|@y8!WYHBKuA3qMC&lfkD zn1m2G91ickefzE=SR_)XIvtUV%I>MAqA0@e_ai$y8~OS9Fc=JIYimPoZ7nn!&9GU| zJI&h%4jecw8wD?b3(;`7n9!8f&_j|WWlu}YoH-M7=gx)6WWw3AXCVke|E{a?R>tIZ zyZxVh@=2qNRc${3R@3F`>+3^tadAJFQ>cc)V1Q1i1JCmiMG@WI-PpT#FZ_N#bUGd4 zxw@vKqr=?T*yuvABP3 zzu#{|aFZ93ot>>qN=h=Mr>7ed5)uqrzxN@exD4Wm&baU92?ySuyl zv(G*|edy4kMgTnkegJSLX?e|-^Zmj{}Mle(S=a|l^YIzN@QcY15rJXx>9yojUtQo*f0RIla6?0>xOtA4j z`4d9W+S=N@b?er@*=#le!6J?$!)C>m+JShU%e-E%XV0EJ2fz8|n{G*xtN^NHX&XNJ z6R#@CFbrHSm#v|pp<&mqU8fP;_u~?PFA(FM^hX46a{gQ2kt0WH&!0cvVX;^M;Qs)8 zg<<&pt-D!OI$Wx%IMsnbz$XZT{pQV^UB`|cyLjQkg?5=%bjpBr#oYi?N)ZSI`txc- zBLtB)AP8MoPy&|s+NgjK0*>RL(P;W(aCW=h)ZEz5{N^Ff%N+n6^KZjf{pnRtMkcKFVk|>G+ zN+}Hl0)CIj<1?Ghw$9E@+x6?$t!A^?Ej!O%`EUWaBF|yH7y*s2EW1MvUDHU^WB}*k y|0l^4IYDp@5;94(h8z|Im)YF%HJ*`1!2cfxMbJWHiy=S&0000 + + + + + DeamonEditor Icons + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + DeamonEditor Icons + + + mfgeg + + + 7.1.2020 + + + + + + + + + + + + + + + + From c2d2361de9130b7dfdf6ce1c9eff2f5331ead3c8 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Wed, 15 Jan 2020 07:26:59 +0300 Subject: [PATCH 17/21] scripts update --- DemonEditor.desktop | 2 +- build-deb.sh | 3 +- deb/DEBIAN/control | 2 +- deb/DEBIAN/copyright | 2 +- deb/usr/bin/{demoneditor.sh => demon-editor} | 0 deb/usr/share/demoneditor/start.py | 4 +++ start.py | 29 ++++++++++++++++++-- 7 files changed, 35 insertions(+), 7 deletions(-) rename deb/usr/bin/{demoneditor.sh => demon-editor} (100%) create mode 100755 deb/usr/share/demoneditor/start.py diff --git a/DemonEditor.desktop b/DemonEditor.desktop index a07d89ea..169857b6 100755 --- a/DemonEditor.desktop +++ b/DemonEditor.desktop @@ -3,7 +3,7 @@ Version=1.0 Name=DemonEditor Comment=Channels and satellites list editor for Enigma2 Comment[ru]=Редактор списка каналов и спутников для Enigma2 -Icon=accessories-text-editor +Icon=demon-editor Exec=bash -c 'cd $(dirname %k) && ./start.py' Terminal=false Type=Application diff --git a/build-deb.sh b/build-deb.sh index 1bae0104..b1894d5a 100755 --- a/build-deb.sh +++ b/build-deb.sh @@ -5,8 +5,7 @@ DEB_PATH="$B_PATH/usr/share/demoneditor" mkdir -p $B_PATH cp -TRv deb $B_PATH -rsync --exclude=app/ui/lang -arv app $DEB_PATH -cp -Rv start.py $DEB_PATH +rsync --exclude=app/ui/lang --exclude=app/ui/icons -arv app $DEB_PATH cd dist fakeroot dpkg-deb --build DemonEditor diff --git a/deb/DEBIAN/control b/deb/DEBIAN/control index e865d1e5..8222946d 100644 --- a/deb/DEBIAN/control +++ b/deb/DEBIAN/control @@ -1,4 +1,4 @@ -Package: DemonEditor +Package: demon-editor Version: 0.4.7-Pre-alpha Section: utils Priority: optional diff --git a/deb/DEBIAN/copyright b/deb/DEBIAN/copyright index 55656b8b..f203617d 100644 --- a/deb/DEBIAN/copyright +++ b/deb/DEBIAN/copyright @@ -5,7 +5,7 @@ Source: https://github.com/DYefremov/DemonEditor Files: * MIT License -Copyright (c) 2018-2019 Dmitriy Yefremov +Copyright (c) 2018-2020 Dmitriy Yefremov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/deb/usr/bin/demoneditor.sh b/deb/usr/bin/demon-editor similarity index 100% rename from deb/usr/bin/demoneditor.sh rename to deb/usr/bin/demon-editor diff --git a/deb/usr/share/demoneditor/start.py b/deb/usr/share/demoneditor/start.py new file mode 100755 index 00000000..cefbe196 --- /dev/null +++ b/deb/usr/share/demoneditor/start.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +from app.ui.main_app_window import start_app + +start_app() diff --git a/start.py b/start.py index cefbe196..e2ca1ee8 100755 --- a/start.py +++ b/start.py @@ -1,4 +1,29 @@ #!/usr/bin/env python3 -from app.ui.main_app_window import start_app +import os -start_app() + +def update_icon(): + need_update = False + icon_name = "DemonEditor.desktop" + + with open(icon_name, "r") as f: + lines = f.readlines() + for i, line in enumerate(lines): + if line.startswith("Icon="): + icon_path = line.lstrip("Icon=") + current_path = "{}/app/ui/icons/hicolor/96x96/apps/demon-editor.png".format(os.getcwd()) + if icon_path != current_path: + need_update = True + lines[i] = "Icon={}\n".format(current_path) + break + + if need_update: + with open(icon_name, "w") as f: + f.writelines(lines) + + +if __name__ == "__main__": + from app.ui.main_app_window import start_app + + update_icon() + start_app() From 0b4e923037ea94b2f477b9759fd1b18212c3785d Mon Sep 17 00:00:00 2001 From: DYefremov Date: Thu, 16 Jan 2020 14:08:34 +0300 Subject: [PATCH 18/21] added callbacks to the player --- app/tools/media.py | 16 +++++++++++++--- app/ui/main_app_window.py | 18 +++++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/app/tools/media.py b/app/tools/media.py index 3479b29c..9ef94dca 100644 --- a/app/tools/media.py +++ b/app/tools/media.py @@ -1,11 +1,12 @@ import sys + from app.commons import run_task, log class Player: __VLC_INSTANCE = None - def __init__(self, rewind_callback, position_callback): + def __init__(self, rewind_callback, position_callback, error_callback, playing_callback): try: from app.tools import vlc from app.tools.vlc import EventType @@ -28,10 +29,19 @@ class Player: lambda et, p: position_callback(p.get_time()), self._player) + if error_callback: + ev_mgr.event_attach(EventType.MediaPlayerEncounteredError, + lambda et, p: error_callback(), + self._player) + if playing_callback: + ev_mgr.event_attach(EventType.MediaPlayerPlaying, + lambda et, p: playing_callback(), + self._player) + @classmethod - def get_instance(cls, rewind_callback=None, position_callback=None): + def get_instance(cls, rewind_callback=None, position_callback=None, error_callback=None, playing_callback=None): if not cls.__VLC_INSTANCE: - cls.__VLC_INSTANCE = Player(rewind_callback, position_callback) + cls.__VLC_INSTANCE = Player(rewind_callback, position_callback, error_callback, playing_callback) return cls.__VLC_INSTANCE @run_task diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index ddc9866c..927a4934 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -270,6 +270,8 @@ class Application(Gtk.Application): 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) + self._fav_view.bind_property("sensitive", self._player_prev_button, "sensitive") + self._fav_view.bind_property("sensitive", self._player_next_button, "sensitive") # Enabling events for the drawing area self._player_drawing_area.set_events(Gdk.ModifierType.BUTTON1_MASK) self._player_frame = builder.get_object("player_frame") @@ -1615,7 +1617,9 @@ class Application(Gtk.Application): if not self._player: try: self._player = Player.get_instance(rewind_callback=self.on_player_duration_changed, - position_callback=self.on_player_time_changed) + position_callback=self.on_player_time_changed, + error_callback=self.on_player_error, + playing_callback=self.set_playback_elms_active) except (ImportError, NameError, AttributeError): self.show_error_dialog("No VLC is found. Check that it is installed!") return @@ -1626,6 +1630,7 @@ class Application(Gtk.Application): self._player_box.set_size_request(w * 0.6, -1) self._player_box.set_visible(True) + self._fav_view.set_sensitive(False) GLib.idle_add(self._player.play, url, priority=GLib.PRIORITY_LOW) def on_player_stop(self, item=None): @@ -1661,6 +1666,8 @@ class Application(Gtk.Application): def on_player_close(self, item=None): if self._player: self._player.stop() + + self.set_playback_elms_active() GLib.idle_add(self._player_box.set_visible, False, priority=GLib.PRIORITY_LOW) @lru_cache(maxsize=1) @@ -1675,6 +1682,15 @@ class Application(Gtk.Application): if not self._full_screen and self._player_rewind_box.get_visible(): GLib.idle_add(self._player_current_time_label.set_text, self.get_time_str(t), priority=GLib.PRIORITY_LOW) + def on_player_error(self): + self.set_playback_elms_active() + self.show_error_dialog("Can't Playback!") + + @run_idle + def set_playback_elms_active(self): + self._fav_view.set_sensitive(True) + self._fav_view.do_grab_focus(self._fav_view) + def get_time_str(self, duration): """ returns a string representation of time from duration in milliseconds """ m, s = divmod(duration // 1000, 60) From 7df7e0b63018b49b3b1c160b7bc51b0023dc8837 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Fri, 17 Jan 2020 00:34:18 +0300 Subject: [PATCH 19/21] changed status update --- app/connections.py | 19 ++++++++++++------- app/ui/main_app_window.py | 38 +++++++++++++++++++++++++++----------- app/ui/main_window.glade | 4 ++-- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/app/connections.py b/app/connections.py index 8d6cdc84..13445d32 100644 --- a/app/connections.py +++ b/app/connections.py @@ -13,7 +13,7 @@ from urllib.parse import urlencode from urllib.request import urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, \ build_opener, install_opener, Request -from app.commons import log +from app.commons import log, run_task from app.settings import SettingsType _BQ_FILES_LIST = ("tv", "radio", # enigma 2 @@ -37,7 +37,7 @@ class DownloadType(Enum): class HttpRequestType(Enum): ZAP = "zap?sRef=" INFO = "about" - SIGNAL = "tunersignal" + SIGNAL = "signal" STREAM = "stream.m3u?ref=" STREAM_CURRENT = "streamcurrent.m3u" CURRENT = "getcurrent" @@ -308,6 +308,7 @@ class HttpAPI: future = self._executor.submit(get_response, req_type, url, self._data) future.add_done_callback(lambda f: callback(f.result())) + @run_task def init(self): user, password = self._settings.http_user, self._settings.http_password use_ssl = self._settings.http_use_ssl @@ -329,17 +330,21 @@ def get_response(req_type, url, data=None): if req_type is HttpRequestType.STREAM or req_type is HttpRequestType.STREAM_CURRENT: 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 + for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"): + return {el.tag: el.text for el in el.iter()} # return first[current] event from the list else: - return {e.tag: e.text for e in ETree.fromstring(f.read().decode("utf-8")).iter()} - except (URLError, HTTPError, RemoteDisconnected, ConnectionResetError) as e: + return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()} + except HTTPError as e: + if req_type is HttpRequestType.TEST: + raise e + return {"error_code": e.code} + except (URLError, RemoteDisconnected, ConnectionResetError) as e: if req_type is HttpRequestType.TEST: raise e except ETree.ParseError as e: log("Parsing response error: {}".format(e)) - return {} + return {"error_code": -1} def init_auth(user, password, url, use_ssl=False): diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index 927a4934..b7721cb3 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -318,6 +318,10 @@ class Application(Gtk.Application): if self._player: self._player.release() + + if self._http_api: + self._http_api.close() + Gtk.Application.do_shutdown(self) def do_command_line(self, command_line): @@ -399,8 +403,6 @@ class Application(Gtk.Application): @run_idle def on_close_app(self, *args): - if self._http_api: - self._http_api.close() self.quit() def on_resize(self, window): @@ -1645,6 +1647,7 @@ class Application(Gtk.Application): if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1): self.set_player_action() + @run_with_delay(1) def set_player_action(self): if self._fav_click_mode is FavClickMode.PLAY: self.on_stream() @@ -1755,6 +1758,10 @@ class Application(Gtk.Application): self.init_send_to(False) return + current_profile = self._profile_combo_box.get_active_text() + if current_profile in self._settings.profiles: + self._settings.current_profile = current_profile + if not self._http_api: self._http_api = HttpAPI(self._settings) GLib.timeout_add_seconds(3, self.update_info, priority=GLib.PRIORITY_LOW) @@ -1840,6 +1847,15 @@ class Application(Gtk.Application): return True def update_receiver_info(self, info): + error_code = info.get("error_code", 0) if info else 0 + GLib.idle_add(self._receiver_info_box.set_visible, error_code == 0, priority=GLib.PRIORITY_LOW) + + if error_code < 0: + return + elif error_code == 412: + self._http_api.init() + return + res_info = info.get("e2about", None) if info else None if res_info: image = info.get("e2distroversion", "") @@ -1849,29 +1865,29 @@ class Application(Gtk.Application): GLib.idle_add(self._receiver_info_label.set_text, info_text, priority=GLib.PRIORITY_LOW) service_name = info.get("e2servicename", None) or "" GLib.idle_add(self._service_name_label.set_text, service_name, priority=GLib.PRIORITY_LOW) - GLib.idle_add(self._signal_box.set_visible, bool(service_name), priority=GLib.PRIORITY_LOW) if service_name: - self.update_service_info(info) - GLib.idle_add(self._receiver_info_box.set_visible, bool(res_info), priority=GLib.PRIORITY_LOW) + self.update_service_info() - @run_idle - def update_service_info(self, info): - has_onid = info.get("e2onid", "N/A") != "N/A" - if has_onid and self._http_api: + def update_service_info(self): + if self._http_api: self._http_api.send(HttpRequestType.SIGNAL, None, self.update_signal) self._http_api.send(HttpRequestType.CURRENT, None, self.update_status) - GLib.idle_add(self._signal_level_bar.set_visible, has_onid) def update_signal(self, sig): 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(int(val.rstrip("%").strip() or 0)) + val = val.strip().rstrip("%") or 0 + with suppress(ValueError): + self._signal_level_bar.set_value(int(val)) + GLib.idle_add(self._signal_level_bar.set_visible, val != "N/A") + @run_idle def update_status(self, evn): if evn: s_duration = evn.get("e2eventstart", 0) + self._service_epg_label.set_visible(bool(s_duration)) if not s_duration: return s_duration = int(s_duration) diff --git a/app/ui/main_window.glade b/app/ui/main_window.glade index 6bc6e2ee..0e3e2bb7 100644 --- a/app/ui/main_window.glade +++ b/app/ui/main_window.glade @@ -3,7 +3,7 @@ The MIT License (MIT) -Copyright (c) 2018-2019 Dmitriy Yefremov +Copyright (c) 2018-2020 Dmitriy Yefremov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -2804,7 +2804,7 @@ Author: Dmitriy Yefremov False start 10 - 2 + 5 True From 136fd118cb089f6c7f848e2a42925d48445354d3 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Sat, 18 Jan 2020 15:28:46 +0300 Subject: [PATCH 20/21] minor fixes --- app/connections.py | 10 ++++++++-- app/ui/main_app_window.py | 6 +++--- app/ui/main_window.glade | 2 +- app/ui/service_details_dialog.py | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/connections.py b/app/connections.py index 13445d32..cad6ba94 100644 --- a/app/connections.py +++ b/app/connections.py @@ -289,8 +289,9 @@ class HttpAPI: def __init__(self, settings): self._settings = settings - self._base_url = None + self._shutdown = False self._session_id = 0 + self._base_url = None self._data = None self.init() @@ -298,6 +299,9 @@ class HttpAPI: self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS) def send(self, req_type, ref, callback=print): + if self._shutdown: + return + url = self._base_url + req_type.value if req_type is HttpRequestType.ZAP or req_type is HttpRequestType.STREAM: @@ -320,8 +324,10 @@ class HttpAPI: if s_id != "0": self._data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8") + @run_task def close(self): - self._executor.shutdown(False) + self._shutdown = True + self._executor.shutdown() def get_response(req_type, url, data=None): diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index b7721cb3..cf10898c 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -1677,9 +1677,9 @@ class Application(Gtk.Application): def on_player_duration_changed(self, duration): self._player_scale.set_value(0) self._player_scale.get_adjustment().set_upper(duration) - GLib.idle_add(self._player_rewind_box.set_visible, duration > 0) - GLib.idle_add(self._player_current_time_label.set_text, "0") - GLib.idle_add(self._player_full_time_label.set_text, self.get_time_str(duration)) + GLib.idle_add(self._player_rewind_box.set_visible, duration > 0, priority=GLib.PRIORITY_LOW) + GLib.idle_add(self._player_current_time_label.set_text, "0", priority=GLib.PRIORITY_LOW) + GLib.idle_add(self._player_full_time_label.set_text, self.get_time_str(duration), priority=GLib.PRIORITY_LOW) def on_player_time_changed(self, t): if not self._full_screen and self._player_rewind_box.get_visible(): diff --git a/app/ui/main_window.glade b/app/ui/main_window.glade index 0e3e2bb7..89ef7033 100644 --- a/app/ui/main_window.glade +++ b/app/ui/main_window.glade @@ -1455,7 +1455,7 @@ Author: Dmitriy Yefremov True - True + False 5 5 player_scale_adjustment diff --git a/app/ui/service_details_dialog.py b/app/ui/service_details_dialog.py index 3b2a8df5..59ebaedb 100644 --- a/app/ui/service_details_dialog.py +++ b/app/ui/service_details_dialog.py @@ -69,7 +69,7 @@ class ServiceDetailsDialog: # Patterns self._DIGIT_PATTERN = re.compile("\\D") self._NON_EMPTY_PATTERN = re.compile("(?:^[\\s]*$|\\D)") - self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-z]{4})(,C:[0-9a-z]{4})*") + self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-fA-F]{1,4})(,C:[0-9a-fA-F]{1,4})*") # Buttons self._apply_button = builder.get_object("apply_button") self._create_button = builder.get_object("create_button") From ea305dadf1ebde8628db0fbbeb50be91a579c883 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Sat, 18 Jan 2020 21:49:21 +0300 Subject: [PATCH 21/21] upd. README --- README.md | 48 ++++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 97ee0bb4..5c0a955e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,21 @@ -# DemonEditor +# DemonEditor ## Enigma2 channel and satellites list editor for GNU/Linux. Experimental support of Neutrino-MP or others on the same basis (BPanther, etc). Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc) + +### Main features of the program: +* Editing bouquets, channels, satellites. +* Import function. +* Backup function. +* Extended support of IPTV. +* Support of picons. +* Downloading of picons and updating of satellites (transponders) from the Internet. +* Import to bouquet(Neutrino WEBTV) from m3u. +* Export of bouquets with IPTV services in m3u. +* Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental). +* Preview (playback) of IPTV or other streams directly from the bouquet list (should be installed [VLC](https://www.videolan.org/vlc/)). + ### Keyboard shortcuts: * **Ctrl + Insert** - copies the selected channels from the main list to the the bouquet beginning or inserts (creates) a new bouquet. @@ -22,36 +35,27 @@ Clipboard is **"rubber"**. There is an accumulation before the insertion! * **Ctrl + Up, Down, PageUp, PageDown, Home, End** - move selected items in the list. * **Ctrl + O** - (re)load user data from current dir. * **Ctrl + D** - load data from receiver. -* **Ctrl + U/B** upload data/bouquets to receiver. - -### Extra: -* Import feature. -* Multiple selections in lists only with Space key (as in file managers). -* Ability to download picons and update satellites (transponders) from web. -* Ability to import into bouquet (Neutrino WEB TV) from m3u. -* Ability to export bouquets with IPTV services to m3u. -* Assignment EPG from DVB or XML for IPTV services (Enigma2 only). -* Preview (playing) IPTV or other streams directly from the bouquet list (should be installed VLC). - +* **Ctrl + U/B** upload data/bouquets to receiver. + ### Minimum requirements: -Python >= 3.5.2 and GTK+ >= 3.16 with PyGObject bindings. - +Python >= 3.5.2 and GTK+ >= 3.16 with PyGObject bindings, python3-requests. + ### Launching: To start the program, in most cases it is enough to download the archive, unpack and run it by double clicking on DemonEditor.desktop in the root directory, or launching from the console with the command: ```./start.py``` Extra folders can be deleted, excluding the *app* folder and root files like *DemonEditor.desktop* and *start.py*! -### Note. -To create a simple **debian package**, you can use the *build-deb.sh.* +### Note: +To create a simple **debian package**, you can use the *build-deb.sh.* +Users of **LTS** versions of [Ubuntu](https://ubuntu.com/) or those based on them can use [PPA](https://launchpad.net/~dmitriy-yefremov/+archive/ubuntu/demon-editor) repository. -The program is tested only with openATV image and Formuler F1 receiver in my favourite Linux distributions. -(the latest versions of Linux Mint 18.* and 19* MATE 64-bit)! +The program is tested only with [openATV](https://www.opena.tv/) image and **Formuler F1** receiver in my favourite Linux distributions +(the latest versions of [Linux Mint](https://linuxmint.com/) 18.* and 19* MATE 64-bit)! -**Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2!** - -**Important:** -Main supported **lamedb** format is version **4**. Versions **3** and **5** has only experimental support! +### Important: +Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2! +Main supported **lamedb** format is version **4**. Versions **3** and **5** has only **experimental** support! For version **3** is only read mode available. When saving, version **4** format is used instead!