diff --git a/app/connections.py b/app/connections.py index bdce6cac..cad6ba94 100644 --- a/app/connections.py +++ b/app/connections.py @@ -1,18 +1,20 @@ -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 Profile +from app.commons import log, run_task +from app.settings import SettingsType _BQ_FILES_LIST = ("tv", "radio", # enigma 2 "myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino @@ -35,21 +37,28 @@ class DownloadType(Enum): class HttpRequestType(Enum): ZAP = "zap?sRef=" INFO = "about" - SIGNAL = "tunersignal" - STREAM = "streamcurrentm3u" - STATUS = "statusinfo" + SIGNAL = "signal" + 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 + TOKEN = "session" 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" 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,15 +103,16 @@ 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) + 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 profile is Profile.ENIGMA_2 and use_http: - ht = http(settings.http_user, settings.http_password, base_url, callback) + if s_type is SettingsType.ENIGMA_2 and use_http: + ht = http(settings.http_user, settings.http_password, base_url, callback, settings.http_use_ssl) next(ht) message = "" if download_type is DownloadType.BOUQUETS: @@ -113,12 +123,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 @@ -139,7 +148,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 +157,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,17 +165,17 @@ 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") + 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() @@ -238,14 +247,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 - 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)) + 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): @@ -276,38 +285,102 @@ def telnet(host, port=23, user="", password="", timeout=5): # ***************** HTTP API *******************# class HttpAPI: + __MAX_WORKERS = 4 - def __init__(self, host, port, user, password): - self._base_url = "http://{}:{}/api/".format(host, port) - init_auth(user, password, self._base_url) + def __init__(self, settings): + self._settings = settings + self._shutdown = False + self._session_id = 0 + self._base_url = None + self._data = 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): + if self._shutdown: + return + 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") - future = self._executor.submit(get_json, req_type, url) + 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 + url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port) + self._base_url = "{}/web/".format(url) + 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") + + @run_task def close(self): - self._executor.shutdown(False) + self._shutdown = True + self._executor.shutdown() -def get_json(req_type, url): +def get_response(req_type, url, data=None): try: - with urlopen(url, timeout=10) as f: - if req_type is HttpRequestType.STREAM: + with urlopen(Request(url, data=data), timeout=10) as f: + 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 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 json.loads(f.read().decode("utf-8")) - except (URLError, HTTPError): - pass + 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 {"error_code": -1} + + +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 *******************# @@ -320,36 +393,30 @@ def test_ftp(host, port, user, password, timeout=5): raise TestException(e) -def test_http(host, port, user, password, timeout=5, 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) - # authentication - init_auth(user, password, url) +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) + data = get_post_data(base_url, password, user) - with urlopen(url, timeout=5) as f: - return json.loads(f.read().decode("utf-8")).get("message", "") + try: + 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): - """ Init authentication """ - pass_mgr = HTTPPasswordMgrWithDefaultRealm() - pass_mgr.add_password(None, url, user, password) - auth_handler = HTTPBasicAuthHandler(pass_mgr) - 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) 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) @@ -358,14 +425,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/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 da318f37..23cfc58f 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,9 +1,11 @@ +import copy import json +import locale import os +from enum import Enum, IntEnum +from pathlib import Path from pprint import pformat from textwrap import dedent -from enum import Enum -from pathlib import Path HOME_PATH = str(Path.home()) CONFIG_PATH = HOME_PATH + "/.config/demon-editor/" @@ -11,19 +13,94 @@ CONFIG_FILE = CONFIG_PATH + "config.json" DATA_PATH = HOME_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(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": {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, + "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 + } + + +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" - 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.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_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/", + "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, "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/", + "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) + def __init__(self, ext_settings=None): + settings = ext_settings or get_settings() + + if self.__VERSION > settings.get("version", 0): + 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: {} @@ -32,8 +109,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): @@ -42,325 +119,350 @@ 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 + set_local_paths(self._cp_settings, self._current_profile) if force_write: - write_config(get_default_settings()) + 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) 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 settings(self): + """ Returns copy of the current settings! """ + return copy.deepcopy(self._settings) - @presets.setter - def presets(self, name): - raise NotImplementedError + @settings.setter + def settings(self, value): + """ Sets copy of the settings! """ + self._settings = copy.deepcopy(value) @property - def profile(self): + def current_profile(self): return self._current_profile - @profile.setter - def profile(self, prf): - self._current_profile = prf - self._config["profile"] = prf.value - self._current_profile_options = self._config.get(prf.value) + @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 + + @profiles.setter + def profiles(self, ps): + self._profiles = ps + self._settings["profiles"] = self._profiles + + @property + def setting_type(self): + return SettingsType(self._cp_settings.get("setting_type", SettingsType.ENIGMA_2.value)) + + @setting_type.setter + def setting_type(self, s_type): + self._cp_settings["setting_type"] = s_type.value + + @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 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._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 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._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/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/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..5fb857b4 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)) @@ -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/dialogs.glade b/app/ui/dialogs.glade index a2d3c1c1..24909e47 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 MacOS. (Experimental) @@ -51,7 +51,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/dialogs.py b/app/ui/dialogs.py index 20a996e3..1cc238a6 100644 --- a/app/ui/dialogs.py +++ b/app/ui/dialogs.py @@ -104,7 +104,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() 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 91d0d9c5..5aa5a48f 100644 --- a/app/ui/download_dialog.py +++ b/app/ui/download_dialog.py @@ -1,18 +1,20 @@ +import os + 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 -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: 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 @@ -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,20 +53,33 @@ 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_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) 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._profile = self._settings.profile - self.init_settings() + self._s_type = self._settings.setting_type + 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) @@ -134,9 +155,11 @@ 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() + 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/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/icons/hicolor/96x96/apps/demon-editor.png b/app/ui/icons/hicolor/96x96/apps/demon-editor.png new file mode 100644 index 00000000..0c560a60 Binary files /dev/null and b/app/ui/icons/hicolor/96x96/apps/demon-editor.png differ diff --git a/app/ui/icons/hicolor/scalable/apps/demon-editor.svg b/app/ui/icons/hicolor/scalable/apps/demon-editor.svg new file mode 100644 index 00000000..451891b4 --- /dev/null +++ b/app/ui/icons/hicolor/scalable/apps/demon-editor.svg @@ -0,0 +1,634 @@ + + + + + + DeamonEditor Icons + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + DeamonEditor Icons + + + mfgeg + + + 7.1.2020 + + + + + + + + + + + + + + + + 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 be1fe941..9ee5140e 100644 --- a/app/ui/iptv.py +++ b/app/ui/iptv.py @@ -10,7 +10,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 @@ -42,7 +42,7 @@ def get_stream_type(box): 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, @@ -90,7 +90,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) @@ -103,7 +103,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: @@ -128,13 +128,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") @@ -171,7 +171,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()), @@ -486,7 +486,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 97e6169c..30856ff8 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -1,7 +1,7 @@ import os import sys - from contextlib import suppress +from datetime import datetime from functools import lru_cache from itertools import chain @@ -9,33 +9,33 @@ 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 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 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): @@ -152,8 +152,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 = [] @@ -182,7 +182,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(self._handlers) self._main_window = builder.get_object("main_window") @@ -210,8 +209,7 @@ class Application(Gtk.Application): self._app_info_box.bind_property("visible", builder.get_object("main_paned"), "visible", 4) self._app_info_box.bind_property("visible", builder.get_object("toolbar_extra_item"), "visible", 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._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") @@ -226,6 +224,7 @@ class Application(Gtk.Application): self._radio_count_label = builder.get_object("radio_count_label") self._data_count_label = builder.get_object("data_count_label") 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) @@ -323,7 +322,15 @@ class Application(Gtk.Application): self.update_profile_label() self.init_drag_and_drop() self.init_colors() - self.init_http_api() + 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) def do_activate(self): self._main_window.set_application(self) @@ -332,9 +339,17 @@ 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() + + if self._http_api: + self._http_api.close() + Gtk.Application.do_shutdown(self) def do_command_line(self, command_line): @@ -347,6 +362,12 @@ class Application(Gtk.Application): self.activate() return 0 + def init_profiles(self, profile=None): + self.update_profiles() + 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 """ target = [] @@ -374,7 +395,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() @@ -410,8 +431,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): @@ -605,7 +624,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 @@ -839,7 +858,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): @@ -855,15 +874,15 @@ 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) - except TestException: + test_http(host, port, user, password, use_ssl=opts.http_use_ssl, skip_message=True) + except (TestException, HttpApiException): use_http = False upload_data(settings=opts, @@ -880,26 +899,27 @@ 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._profile_combo_box.set_sensitive(False) 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!") @@ -917,6 +937,9 @@ 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 def append_data(self, bouquets, services): @@ -981,6 +1004,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 @@ -1019,6 +1053,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 @@ -1039,9 +1074,9 @@ class Application(Gtk.Application): GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) def save_data(self): - 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 @@ -1061,7 +1096,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) @@ -1075,10 +1110,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) yield True @@ -1088,7 +1123,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): @@ -1098,12 +1133,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]) @@ -1171,7 +1206,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 @@ -1196,25 +1231,49 @@ class Application(Gtk.Application): for v in [view, *args]: v.get_selection().unselect_all() - def on_preferences(self, action, value): + def on_settings(self, action, value): 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.profile - self._ip_label.set_text(self._settings.host) - if profile != self._profile: + def update_settings(self): + s_type = self._settings.setting_type + + if s_type != self._s_type: yield from self.show_app_info(True) - self._profile = 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, entry): + if self._app_info_box.get_visible(): + self.update_profile_label() + return + + active = self._profile_combo_box.get_active_text() + if active in self._settings.profiles: + 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: + self._profile_combo_box.append(p, p) def on_tree_view_key_press(self, view, event): """ Handling keystrokes on press """ @@ -1312,7 +1371,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: @@ -1328,17 +1387,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) @@ -1349,9 +1408,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) @@ -1400,10 +1459,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) @@ -1414,14 +1475,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, action, value): - 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 @@ -1435,7 +1496,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, action, value=None): @@ -1451,7 +1512,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) @@ -1459,7 +1520,7 @@ class Application(Gtk.Application): @run_idle def on_epg_list_configuration(self, action, value=None): - 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 @@ -1477,7 +1538,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, action, value=None): """ Imports iptv from m3u files. """ @@ -1489,7 +1550,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) @@ -1520,7 +1581,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: @@ -1532,7 +1593,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, action, value=None): @@ -1573,7 +1634,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 @@ -1583,7 +1644,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 @@ -1594,6 +1657,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): @@ -1602,10 +1666,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_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.set_player_action() + + @run_with_delay(1) + 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): @@ -1621,21 +1694,32 @@ 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) 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(): 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) @@ -1684,30 +1768,35 @@ class Application(Gtk.Application): if not state & Gdk.WindowState.ICONIFIED and self._links_transmitter: self._links_transmitter.hide() - # ************************ HTTP API ****************************# + # ************************ 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 - status = all((http_api_enable, self._profile is Profile.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._profile is Profile.NEUTRINO_MP or not http_api_enable: - self.update_info_boxes_visible(False) + if self._s_type is SettingsType.NEUTRINO_MP or not http_api_enable: + GLib.idle_add(self._receiver_info_box.set_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) + 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) + else: + self._http_api.init() self.init_send_to(http_api_enable and self._settings.enable_send_to) + yield True @run_idle def init_send_to(self, enable): @@ -1716,9 +1805,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: @@ -1733,67 +1836,94 @@ 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("result", 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 """ - 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 - 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) - GLib.idle_add(self._receiver_info_label.set_text, info_text) - GLib.idle_add(self._receiver_info_box.set_visible, bool(res_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) - 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)) + 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", "") + 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, 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) + if service_name: + self.update_service_info() + + 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) 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) + 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") - def update_status(self, status): - if status: - dsc = "{} {} - {}".format(status.get("currservice_name", ""), - status.get("currservice_begin", ""), - status.get("currservice_end", "")) + @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) + s_time = datetime.fromtimestamp(s_duration) + 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(status.get("currservice_description", "")) + self._service_epg_label.set_tooltip_text(evn.get("e2eventdescription", "")) # ***************** Filter and search *********************# @@ -1817,7 +1947,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 @@ -1834,7 +1964,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))) @@ -1913,7 +2043,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) @@ -2031,7 +2161,7 @@ class Application(Gtk.Application): @run_idle def on_picons_loader_show(self, action, value): 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] @@ -2041,7 +2171,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): @@ -2094,33 +2224,40 @@ 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 *********************# + @run_idle def update_profile_label(self): - if self._profile is Profile.ENIGMA_2: - ver = self.get_format_version() - self._main_window.set_title("DemonEditor [{} Enigma2 v.{}]".format(get_message("Profile:"), ver)) - elif self._profile is Profile.NEUTRINO_MP: - self._main_window.set_title("DemonEditor [{} 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._main_window.set_title.set_subtitle("DemonEditor [{} {} Enigma2 v.{}]".format(msg, profile_name, self.get_format_version())) + elif self._s_type is SettingsType.NEUTRINO_MP: + self._main_window.set_title("DemonEditor [{} {} Neutrino-MP]".format(msg, profile_name)) 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) 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) + Settings.reset_to_default() + 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..a7ca724e 100644 --- a/app/ui/main_helper.py +++ b/app/ui/main_helper.py @@ -9,9 +9,9 @@ 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 .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column +from app.settings import SettingsType 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): @@ -382,7 +384,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 +466,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 @@ -487,12 +489,15 @@ 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 *********************# -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 +507,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 +516,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 +588,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 0e634fda..617b5028 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 @@ -26,7 +26,7 @@ THE SOFTWARE. Author: Dmitriy Yefremov --> - + @@ -674,9 +674,10 @@ Author: Dmitriy Yefremov 640 + 480 False center - accessories-text-editor + demon-editor center DemonEditor @@ -963,6 +964,9 @@ Author: Dmitriy Yefremov True True + False + 5 + 5 player_scale_adjustment False 0 @@ -2207,7 +2211,6 @@ Author: Dmitriy Yefremov True False - 2 False @@ -2240,7 +2243,7 @@ Author: Dmitriy Yefremov True False - gtk-edit + demon-editor 6 @@ -2289,14 +2292,13 @@ Author: Dmitriy Yefremov - 30 False False start 10 - 2 + 5 True @@ -2333,62 +2335,56 @@ Author: Dmitriy Yefremov - + True False + False + Current IP: center - 2 - - - True + center + 1 + 1 + 0 + True + + False - gtk-connect + True + baseline + baseline + 1 + 1 + False + False + 9 + True + False + gtk-connect + - - False - True - 0 - - - - - True - False - center - Current IP: - - - - - - 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 @@ -2477,21 +2473,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..59ebaedb 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 @@ -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") @@ -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..0a7aaf05 100644 --- a/app/ui/settings_dialog.glade +++ b/app/ui/settings_dialog.glade @@ -26,13 +26,88 @@ THE SOFTWARE. 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 @@ -47,7 +122,6 @@ Author: Dmitriy Yefremov 1 False - False True center-on-parent True @@ -55,10 +129,13 @@ Author: Dmitriy Yefremov True True center + True False + Options + Settings type: 2 True @@ -73,7 +150,7 @@ Author: Dmitriy Yefremov True False - gtk-apply + gtk-ok @@ -98,91 +175,36 @@ 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 - - + 1 + 1 + + 3 + + + + + True + True + True + Apply profile settings + + + + True + False + gtk-apply + + + + + + 4 + @@ -236,7 +258,7 @@ Author: Dmitriy Yefremov True False - + 150 True False @@ -253,75 +275,340 @@ Author: Dmitriy Yefremov True False 5 + - + True - False - 6 - 5 - vertical + True + True - + True False + 5 + 5 0.019999999552965164 in - + + 155 True False - 5 - 5 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 + 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 + 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 + False + 5 + 5 + 5 + 0.019999999552965164 + in + + + True + False + 5 + 5 + vertical + 2 + + True False - center - settings_stack + 5 + 2 + 2 + True + + + True + True + 127.0.0.1 + network-transmit-receive-symbolic + + + 0 + 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 @@ -330,23 +617,393 @@ Author: Dmitriy Yefremov - + True False + 5 - + + 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 + 5 + + + + True + False + 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 + + + + + True + False + Port: + + + 2 + 0 + + + + + ftp + FTP + + + + + True + False + 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 + 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 @@ -354,646 +1011,409 @@ Author: Dmitriy Yefremov 2 + + + + + True + False + Network settings: + + + + + True + True + 1 + + + + + True + False + 5 + 5 + 0.019999999552965164 + in + + + True + False + 5 + 5 + 5 + 2 + True - - Test - 110 + + True + False + Services and Bouquets files: + 0 + + + 0 + 0 + + + + True True - True - Test connection - test_button_image - True - + /etc/enigma2/ + gtk-edit - False - True - end - 3 + 0 + 1 - - True + False - - - + User bouquet files: + 2.2351741291171123e-10 - True - True - 4 + 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 - - False - True - 1 - - - + + True False + STB file paths: + + + + + True + True + 2 + + + + + True + False + 5 + 5 + 5 + 0.019999999552965164 + in + + + True + False + 5 + 5 + 5 + 2 + True - + 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 - - + Picons path: + 0.019999999552965164 - ftp - FTP + 0 + 2 - + 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 - - + True + gtk-edit + folder-open + False + Select + Select + - http - HTTP - 1 + 0 + 3 - + 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 - - + Data path: + 0 + 0.019999999552965164 - telnet - Telnet - 2 + 0 + 0 + + + + + True + True + gtk-edit + folder-open + False + Select + Select + + + + 0 + 1 + + + + + True + False + Backup path: + 0.019999999552965164 + + + 0 + 4 + + + + + True + True + gtk-edit + folder-open + False + Select + Select + + + + 0 + 5 - - False - True - 1 - + + + + True + False + Local file paths: + - - - - True - False - Network settings: - + + True + True + 3 + - True - True - 0 + True + False - network + profiles 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 + 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 + 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 @@ -1074,9 +1494,11 @@ Author: Dmitriy Yefremov + 250 True True True + end True @@ -1098,9 +1520,11 @@ Author: Dmitriy Yefremov + 250 True True True + end True @@ -1124,7 +1548,7 @@ Author: Dmitriy Yefremov False True - 0 + 2 @@ -1206,14 +1630,14 @@ Author: Dmitriy Yefremov False True - 1 + 3 program Program - 2 + 1 @@ -1450,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 @@ -1496,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 + + @@ -1525,7 +1967,7 @@ Author: Dmitriy Yefremov extra Extra - 3 + 2 @@ -1537,7 +1979,7 @@ Author: Dmitriy Yefremov - False + True True 0 diff --git a/app/ui/settings_dialog.py b/app/ui/settings_dialog.py index a86d7eb1..00a02485 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 Profile -from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, FavClickMode -from .main_helper import update_entry_data +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 +from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON def show_settings_dialog(transient, options): @@ -19,26 +22,42 @@ 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_profile_changed": self.on_profile_changed, + "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_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, "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_selected": self.on_profile_selected, + "on_profile_set_default": self.on_profile_set_default, + "on_lang_changed": self.on_lang_changed, + "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() - builder.set_translation_domain(TEXT_DOMAIN) builder.add_from_file(UI_RESOURCES_PATH + "settings_dialog.glade") builder.connect_signals(handlers) 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") @@ -47,6 +66,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") @@ -64,7 +84,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") @@ -77,6 +97,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") @@ -85,46 +106,77 @@ 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") + # 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._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._active_profile = settings.profile + self._ext_settings = settings + self._settings = Settings(settings.settings) + self._profiles = self._settings.profiles + self._s_type = self._settings.setting_type self.set_settings() - self.init_ui_elements(self._active_profile) + self.init_ui_elements(self._s_type) + self.init_profiles() - 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._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) - def show(self): - response = self._dialog.run() - if response == Gtk.ResponseType.OK: - self.apply_settings() - self._dialog.destroy() + 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) - return response + 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): + self._dialog.run() + + 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) - 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 - self.set_settings() + 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.on_reset() self.init_ui_elements(profile) - def on_reset(self, item): + def on_reset(self, item=None): self._settings.reset() self.set_settings() @@ -136,6 +188,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) @@ -144,14 +197,15 @@ 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) + self._load_on_startup_switch.set_active(self._settings.load_last_config) - 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) @@ -164,9 +218,9 @@ class SettingsDialog: self._new_color_button.set_rgba(new_rgb) 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 + 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() self._settings.port = self._port_field.get_text() self._settings.user = self._login_field.get_text() @@ -174,6 +228,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() @@ -182,23 +237,33 @@ 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.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.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() - if self._active_profile is Profile.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() + def apply_settings(self, item=None): + if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL: + return - self._settings.save() + 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() + 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() + 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._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 def on_connection_test(self, item): @@ -216,10 +281,13 @@ 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) + except HttpApiException as e: + self.show_info_message(str(e), Gtk.MessageType.WARNING) finally: self.show_spinner(False) @@ -263,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): @@ -272,6 +342,123 @@ 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] = 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 + self.on_reset() + + 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() + 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_value): + p_name = render.get_property("text") + 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 != new_value: + self.update_local_paths(new_value) + self.on_profile_selected(self._profile_view) + + 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 + + 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() + 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): + 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) + + 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) + + 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): + 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 + + 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) @@ -279,17 +466,24 @@ 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 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 diff --git a/app/ui/transmitter.py b/app/ui/transmitter.py index 16cc7a96..d418f1eb 100644 --- a/app/ui/transmitter.py +++ b/app/ui/transmitter.py @@ -87,7 +87,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): diff --git a/app/ui/uicommons.py b/app/ui/uicommons.py index 97c57a33..ef977f17 100644 --- a/app/ui/uicommons.py +++ b/app/ui/uicommons.py @@ -6,6 +6,10 @@ import sys import gi from enum import Enum, IntEnum +import gi + +from app.settings import Settings, SettingsException + gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk @@ -21,12 +25,22 @@ IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID"))) # translation os.environ["LANG"] = "{}.{}".format(*locale.getlocale()) TEXT_DOMAIN = "demon-editor" +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") LANG_PATH = GTK_PATH + "/share/locale" if GTK_PATH else UI_RESOURCES_PATH + "lang" gettext.bindtextdomain(TEXT_DOMAIN, LANG_PATH) if sys.platform != "darwin": locale.bindtextdomain(TEXT_DOMAIN, LANG_PATH) 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 @@ -36,6 +50,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): @@ -94,6 +109,7 @@ class FavClickMode(IntEnum): STREAM = 1 PLAY = 2 ZAP = 3 + ZAP_PLAY = 4 class ViewTarget(Enum): 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/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 00000000..0c560a60 Binary files /dev/null and b/deb/usr/share/icons/hicolor/96x96/apps/demon-editor.png differ diff --git a/deb/usr/share/icons/hicolor/scalable/apps/demon-editor.svg b/deb/usr/share/icons/hicolor/scalable/apps/demon-editor.svg new file mode 100644 index 00000000..451891b4 --- /dev/null +++ b/deb/usr/share/icons/hicolor/scalable/apps/demon-editor.svg @@ -0,0 +1,634 @@ + + + + + + DeamonEditor Icons + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + DeamonEditor Icons + + + mfgeg + + + 7.1.2020 + + + + + + + + + + + + + + + +