mirror of
https://github.com/DYefremov/DemonEditor.git
synced 2026-05-08 20:56:23 +02:00
Compare commits
208 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b45dda8ada | ||
|
|
e1577d8e0c | ||
|
|
a184a7cc7f | ||
|
|
6a4ca77009 | ||
|
|
c1ed748a91 | ||
|
|
d6791a9c89 | ||
|
|
1f0411fb3d | ||
|
|
27a1838980 | ||
|
|
c1bfb482e1 | ||
|
|
411e012f5c | ||
|
|
a7bc32d7ae | ||
|
|
ef3f69ece1 | ||
|
|
344f4905fc | ||
|
|
51f36d14ec | ||
|
|
f2b31b2ac4 | ||
|
|
118734d7fb | ||
|
|
d7b7f6571b | ||
|
|
9728843b0a | ||
|
|
033ac70c8a | ||
|
|
85247e8307 | ||
|
|
d38a896685 | ||
|
|
880908c163 | ||
|
|
ec94d5ef46 | ||
|
|
49f9863922 | ||
|
|
6d4249cf1e | ||
|
|
0fc0ef1d3e | ||
|
|
c587f2bcdc | ||
|
|
5cd8c68589 | ||
|
|
61690db0ee | ||
|
|
fcc2b6b6a8 | ||
|
|
a5412cd2b3 | ||
|
|
a47a7417c2 | ||
|
|
bdac77e88c | ||
|
|
8ab79a2937 | ||
|
|
8dc880577f | ||
|
|
b1829651d3 | ||
|
|
7339872de6 | ||
|
|
8155643098 | ||
|
|
87a1cde859 | ||
|
|
a591d31d01 | ||
|
|
e863c41117 | ||
|
|
811539ae19 | ||
|
|
bc7327a6d5 | ||
|
|
0c114964f2 | ||
|
|
b8a3e5e4c1 | ||
|
|
1d16e9e220 | ||
|
|
c96b464cbc | ||
|
|
43821e6f50 | ||
|
|
e0e642db5a | ||
|
|
2266fd4d3d | ||
|
|
6b8145c674 | ||
|
|
fa89ab8608 | ||
|
|
da70b0fb18 | ||
|
|
6932465cfd | ||
|
|
303fe0b1ae | ||
|
|
f8dec3140c | ||
|
|
60e1e4f5e6 | ||
|
|
a99d6e26db | ||
|
|
ecf5b6399c | ||
|
|
3c04a00230 | ||
|
|
527a52c87b | ||
|
|
4bcd126947 | ||
|
|
1a3617a6d4 | ||
|
|
d6d7b105ec | ||
|
|
eab34c5ecc | ||
|
|
3cef063aa4 | ||
|
|
958320e573 | ||
|
|
394b7c4c01 | ||
|
|
00f492b0a2 | ||
|
|
0e0abdcf8e | ||
|
|
037b917d3e | ||
|
|
0bc85cb5fa | ||
|
|
8bf6427bbd | ||
|
|
f85c1d2e0d | ||
|
|
03e2fc96ec | ||
|
|
f0d58c0fb4 | ||
|
|
1fc10f0119 | ||
|
|
a21f6faab2 | ||
|
|
d78cee1241 | ||
|
|
145bd75776 | ||
|
|
6765fd5db7 | ||
|
|
53616f95b0 | ||
|
|
137b5acde5 | ||
|
|
17f705a4e3 | ||
|
|
d68a215e2a | ||
|
|
d8f67380e5 | ||
|
|
9965f3e3a5 | ||
|
|
2bb0faa19e | ||
|
|
9c5cf8cebb | ||
|
|
fb20e82572 | ||
|
|
ffdf5d8ce2 | ||
|
|
8b6f860459 | ||
|
|
c747cf1275 | ||
|
|
7a71ebd188 | ||
|
|
c4847766bb | ||
|
|
73a611dc3c | ||
|
|
ef931bcd75 | ||
|
|
f173587dab | ||
|
|
9a0b362b91 | ||
|
|
51acb171d5 | ||
|
|
b57adb43ba | ||
|
|
bcea538c4e | ||
|
|
77281271c8 | ||
|
|
5c94912f21 | ||
|
|
e8f33cbee9 | ||
|
|
aa2b06ea27 | ||
|
|
5576bd8112 | ||
|
|
551c9d5722 | ||
|
|
f6518f1ee5 | ||
|
|
20b534f723 | ||
|
|
82a954e1a4 | ||
|
|
67446f0898 | ||
|
|
39a092cb57 | ||
|
|
4d81937779 | ||
|
|
68dc48cdbe | ||
|
|
1a6be14949 | ||
|
|
7295ec90c0 | ||
|
|
3a98b497c8 | ||
|
|
12bb1f0601 | ||
|
|
eebe953ac2 | ||
|
|
741bea29e6 | ||
|
|
97041e5799 | ||
|
|
5ee4e18346 | ||
|
|
de508fbfc2 | ||
|
|
36aebe7f19 | ||
|
|
5ac9053944 | ||
|
|
ce6819d539 | ||
|
|
b13c2f321c | ||
|
|
015b6b1ccd | ||
|
|
911279ce09 | ||
|
|
71ddd12541 | ||
|
|
4867b1b648 | ||
|
|
25fba17b9c | ||
|
|
f77a55eadd | ||
|
|
b6e73e5e7a | ||
|
|
780bda1f12 | ||
|
|
a4a44692e2 | ||
|
|
6db03b6cac | ||
|
|
a94c53a9c9 | ||
|
|
b012fccd1a | ||
|
|
4062d206b8 | ||
|
|
a1f656fbca | ||
|
|
84afaee1d0 | ||
|
|
08619dd182 | ||
|
|
04f27eff88 | ||
|
|
6e706dec2d | ||
|
|
3bf787b9fb | ||
|
|
3b1bb80d3c | ||
|
|
05fa5eaf11 | ||
|
|
b558a17d9d | ||
|
|
0ee248a24f | ||
|
|
3a368427fd | ||
|
|
384c30ea18 | ||
|
|
05cf047127 | ||
|
|
621b090a1a | ||
|
|
a8d3f39442 | ||
|
|
02c261b4dd | ||
|
|
5c3532db65 | ||
|
|
fda9780de9 | ||
|
|
6c5bd5d576 | ||
|
|
9c5b7a3901 | ||
|
|
b7f312a35d | ||
|
|
9401b2a7f7 | ||
|
|
682fa341d0 | ||
|
|
c9daa8a599 | ||
|
|
94d3d0d9ac | ||
|
|
2189997122 | ||
|
|
8397efa324 | ||
|
|
d21f9410cd | ||
|
|
be9b3178e0 | ||
|
|
2a8ddc093c | ||
|
|
fa1ec4cdcf | ||
|
|
384da95988 | ||
|
|
960541b56a | ||
|
|
396d10a805 | ||
|
|
30e1c63a47 | ||
|
|
ef7e35378d | ||
|
|
0a1bbab7d0 | ||
|
|
65502018a0 | ||
|
|
cc20042001 | ||
|
|
50c2e831ce | ||
|
|
ea91c39769 | ||
|
|
3dab8ef7b7 | ||
|
|
dd1a543e5c | ||
|
|
0966489024 | ||
|
|
052187359d | ||
|
|
6ca6867ea9 | ||
|
|
d9cdc6458c | ||
|
|
70b9851324 | ||
|
|
2a3b558d83 | ||
|
|
21ea841f34 | ||
|
|
1db0ce3fc5 | ||
|
|
2804a9bc54 | ||
|
|
8976f42974 | ||
|
|
8330104f3c | ||
|
|
3ede2e2b07 | ||
|
|
dd796c0f88 | ||
|
|
f3a432c002 | ||
|
|
4c1cdc4850 | ||
|
|
a062d74e0e | ||
|
|
7bf36c8d6d | ||
|
|
6aad0344c8 | ||
|
|
bb4665d180 | ||
|
|
a455c4569d | ||
|
|
8d5af301fb | ||
|
|
f342b99769 | ||
|
|
a0612e6a98 | ||
|
|
60b8f7642d |
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2026 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
|
||||
|
||||
21
README.md
21
README.md
@@ -41,10 +41,7 @@ Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
|
||||
* **Ctrl + Alt + R** - rename for bouquet.
|
||||
* **Ctrl + S, T** in Satellites edit tool for create satellite or transponder.
|
||||
* **Ctrl + L** - parental lock.
|
||||
* **Ctrl + H** - hide/skip.
|
||||
* **Ctrl + P** - start play IPTV or other stream in the bouquet list.
|
||||
* **Ctrl + Z** - switch(**zap**) the channel(works when the HTTP API is enabled, Enigma2 only).
|
||||
* **Ctrl + W** - switch to the channel and watch in the program.
|
||||
* **Ctrl + H** - hide/skip.
|
||||
* **Space** - select/deselect.
|
||||
* **Left/Right** - remove selection.
|
||||
* **Ctrl + Up, Down, PageUp, PageDown, Home, End**- move selected items in the list.
|
||||
@@ -56,13 +53,16 @@ Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
|
||||
* **Ctrl + Shift + F** - show/hide filter bar.
|
||||
* **Ctrl + T** - show/hide built-in Telnet client.
|
||||
* **Ctrl + Shift + L** - show/hide logging panel.
|
||||
* **Shift + P** - start play IPTV or other stream in the bouquet list.
|
||||
* **Shift + Z** - switch(**zap**) the channel(works when the HTTP API is enabled, Enigma2 only).
|
||||
* **Shift + W** - switch to the channel and watch in the program.
|
||||
|
||||
For **multiple** selection with the mouse, press and hold the **Ctrl** key!
|
||||
|
||||
## Minimum requirements
|
||||
*Python >= 3.6, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.*
|
||||
|
||||
***Optional:** python3-pil, python3-chardet.*
|
||||
***Optional:** python3-pil, python3-chardet, ffmpeg.*
|
||||
## Installation and Launch
|
||||
* ### Linux
|
||||
To start the program, in most cases it is enough to download the [archive](https://github.com/DYefremov/DemonEditor/archive/master.zip), unpack
|
||||
@@ -77,9 +77,11 @@ A ready-made [package](https://aur.archlinux.org/packages/demoneditor-bin) is al
|
||||
**This program can be run on macOS.**
|
||||
To run the program on macOS, you need to install [Homebrew](https://brew.sh/).
|
||||
Then install the required components via terminal:
|
||||
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme python-requests gtksourceview3```
|
||||
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme gtksourceview3```
|
||||
|
||||
*Optional:* ```brew install pillow python-chardet```
|
||||
```pip3 install requests telnetlib-313-and-up --break-system-packages```
|
||||
|
||||
*Optional:* ```brew install pillow python-chardet ffmpeg```
|
||||
|
||||
Launch is similar to Linux.
|
||||
|
||||
@@ -99,15 +101,12 @@ THIS SOFTWARE COMES WITH ABSOLUTELY NO WARRANTY.
|
||||
AUTHOR IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY CONNECTION WITH THIS SOFTWARE.
|
||||
|
||||
## Important
|
||||
The program is tested only with [openATV](https://www.opena.tv/) image and **Formuler F1** receiver in [Linux Mint](https://linuxmint.com/) (MATE 64-bit) distribution!
|
||||
Support for DVB-T/T2 and DVB-C channels for Neutrino is not fully implemented and has an experimental status.
|
||||
|
||||
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support! For version **3** is only read mode available. When saving, version **4** format is used instead.
|
||||
|
||||
When using the multiple import feature, from *lamedb* will be taken data **only for channels that are in the selected bouquets!**
|
||||
If you need full set of the data, including *[satellites, terrestrial, cables].xml* (current files will be overwritten),
|
||||
just load your data via *"File/Open"* and press *"Save"*. When importing separate bouquet files, only those services
|
||||
(excluding IPTV) that are in the **current open lamedb** (main list of services) will be imported.
|
||||
When importing separate bouquet files, only those services (excluding IPTV) that are in the **current open lamedb** (main list of services) will be imported.
|
||||
|
||||
**The built-in Telnet client does not support ANSI escape sequences!**
|
||||
|
||||
|
||||
@@ -1,9 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2026 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
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov <https://github.com/DYefremov>
|
||||
#
|
||||
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from functools import wraps
|
||||
from threading import Thread, Timer
|
||||
from threading import Timer
|
||||
|
||||
from gi.repository import GLib
|
||||
from gi.repository.Gio import Task
|
||||
|
||||
_LOG_FILE = "demon-editor.log"
|
||||
LOG_DATE_FORMAT = "%d-%m-%y %H:%M:%S"
|
||||
@@ -41,12 +70,12 @@ def run_idle(func):
|
||||
|
||||
|
||||
def run_task(func):
|
||||
""" Runs function in separate thread """
|
||||
""" Runs a function in a separate thread. """
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
task = Thread(target=func, args=args, kwargs=kwargs, daemon=True)
|
||||
task.start()
|
||||
task = Task()
|
||||
task.run_in_thread(lambda t, s, d, c: func(*args, **kwargs))
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2025 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
|
||||
@@ -28,15 +28,15 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
import selectors
|
||||
import socket
|
||||
import time
|
||||
import urllib
|
||||
import xml.etree.ElementTree as ETree
|
||||
from enum import Enum
|
||||
from ftplib import FTP, CRLF, Error, all_errors
|
||||
from ftplib import FTP, FTP_PORT, CRLF, Error, all_errors
|
||||
from http.client import RemoteDisconnected
|
||||
from pathlib import Path
|
||||
from telnetlib import Telnet
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlencode, quote
|
||||
from urllib.request import (urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener,
|
||||
@@ -48,7 +48,7 @@ from app.settings import SettingsType
|
||||
BQ_FILES_LIST = ("tv", "radio", # Enigma2.
|
||||
"services.xml", "myservices.xml", "bouquets.xml", "ubouquets.xml") # Neutrino.
|
||||
|
||||
DATA_FILES_LIST = ("lamedb", "lamedb5", "blacklist", "whitelist",)
|
||||
DATA_FILES_LIST = ("lamedb", "lamedb5", "blacklist", "whitelist", "whitelist_streamrelay")
|
||||
|
||||
STC_XML_FILE = ("satellites.xml", "terrestrial.xml", "cables.xml")
|
||||
WEB_TV_XML_FILE = ("webtv.xml", "webtv_usr.xml")
|
||||
@@ -59,10 +59,15 @@ PICONS_MAX_NUM = 1000 # Maximum picon number for sending without compression.
|
||||
class DownloadType(Enum):
|
||||
ALL = 0
|
||||
BOUQUETS = 1
|
||||
SATELLITES = 2
|
||||
PICONS = 3
|
||||
WEBTV = 4
|
||||
EPG = 5
|
||||
SERVICES = 2
|
||||
SATELLITES = 3
|
||||
PICONS = 4
|
||||
WEBTV = 5
|
||||
EPG = 6
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.ALL
|
||||
|
||||
|
||||
class TestException(Exception):
|
||||
@@ -73,9 +78,66 @@ class HttpApiException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class StubTelnet:
|
||||
""" Stub class for Telnet.
|
||||
|
||||
Used to run a program on an OS with Python >= 3.13
|
||||
without the need to install telnetlib .
|
||||
-> https://github.com/DYefremov/DemonEditor/issues/218.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._msg = "Please (re)install [telnetlib] module. -> [https://github.com/DYefremov/DemonEditor/issues/218]"
|
||||
log(self._msg)
|
||||
|
||||
def read_until(self, match, timeout=None):
|
||||
raise TestException(self._msg)
|
||||
|
||||
|
||||
TN = StubTelnet
|
||||
|
||||
try:
|
||||
from telnetlib import Telnet
|
||||
except ModuleNotFoundError as e:
|
||||
log(e)
|
||||
else:
|
||||
TN = Telnet
|
||||
|
||||
|
||||
class ExtTelnet(TN):
|
||||
|
||||
def __init__(self, output_callback=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._output_callback = output_callback
|
||||
|
||||
def interact(self):
|
||||
""" Interaction function, emulates a very dumb telnet client. """
|
||||
with selectors.DefaultSelector() as selector:
|
||||
selector.register(self, selectors.EVENT_READ)
|
||||
|
||||
while True:
|
||||
for key, events in selector.select():
|
||||
if key.fileobj is self:
|
||||
try:
|
||||
text = self.read_very_eager()
|
||||
except EOFError as e:
|
||||
msg = "\n*** Connection closed by remote host ***\n"
|
||||
if self._output_callback:
|
||||
self._output_callback(msg)
|
||||
log(msg)
|
||||
raise e
|
||||
else:
|
||||
if text and self._output_callback:
|
||||
self._output_callback(text)
|
||||
|
||||
|
||||
class UtfFTP(FTP):
|
||||
""" FTP class wrapper. """
|
||||
|
||||
def __init__(self, *, host="", port=FTP_PORT, user="", passwd="", **kwargs):
|
||||
self.port = port
|
||||
super().__init__(host, user, passwd, **kwargs)
|
||||
|
||||
def retrlines(self, cmd, callback=None):
|
||||
""" Small modification of the original method.
|
||||
|
||||
@@ -367,19 +429,21 @@ class UtfFTP(FTP):
|
||||
|
||||
|
||||
def download_data(*, settings, download_type=DownloadType.ALL, callback=log, files_filter=None):
|
||||
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
|
||||
with UtfFTP(host=settings.host, port=settings.port, user=settings.user, passwd=settings.password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
callback("FTP OK.")
|
||||
save_path = settings.profile_data_path
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
# bouquets
|
||||
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
|
||||
if download_type in (DownloadType.ALL, DownloadType.BOUQUETS, DownloadType.SERVICES):
|
||||
ftp.cwd(settings.services_path)
|
||||
file_list = BQ_FILES_LIST + DATA_FILES_LIST if download_type is DownloadType.ALL else BQ_FILES_LIST
|
||||
file_list = BQ_FILES_LIST
|
||||
if download_type is DownloadType.ALL or DownloadType.SERVICES:
|
||||
file_list += DATA_FILES_LIST
|
||||
ftp.download_files(save_path, file_list, callback)
|
||||
# *.xml and webtv
|
||||
if download_type in (DownloadType.ALL, DownloadType.SATELLITES):
|
||||
ftp.download_xml(save_path, settings.satellites_xml_path, STC_XML_FILE, callback)
|
||||
ftp.download_xml(save_path, settings.satellites_xml_path, files_filter or STC_XML_FILE, callback)
|
||||
if download_type in (DownloadType.ALL, DownloadType.WEBTV):
|
||||
ftp.download_xml(save_path, settings.satellites_xml_path, WEB_TV_XML_FILE, callback)
|
||||
|
||||
@@ -396,16 +460,17 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=log, fil
|
||||
|
||||
|
||||
def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_callback=None,
|
||||
files_filter=None, ext_host=None):
|
||||
files_filter=None, ext_host=None, ext_path=None):
|
||||
s_type = settings.setting_type
|
||||
use_http = s_type is SettingsType.ENIGMA_2 and settings.use_http
|
||||
data_path = settings.profile_data_path
|
||||
host, port, use_ssl = ext_host or settings.host, settings.http_port, settings.http_use_ssl
|
||||
user, password = settings.user, settings.password
|
||||
base_url = f"http{'s' if use_ssl else ''}://{host}:{port}"
|
||||
base = "web" if s_type is SettingsType.ENIGMA_2 else "control"
|
||||
url = f"{base_url}/{base}/"
|
||||
tn, ht = None, None # Telnet, HTTP.
|
||||
ftp_port, telnet_port = settings.port, settings.telnet_port
|
||||
data_path = ext_path or settings.profile_data_path
|
||||
|
||||
try:
|
||||
use_http = use_http and test_http(host, port, user, password, use_ssl=use_ssl, skip_message=True, s_type=s_type)
|
||||
@@ -426,7 +491,7 @@ def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_
|
||||
|
||||
ht.send((f"{url}message?{params}", "Sending info message... "))
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2 and download_type is DownloadType.ALL:
|
||||
if s_type is SettingsType.ENIGMA_2 and download_type in (DownloadType.ALL, DownloadType.SERVICES):
|
||||
time.sleep(5)
|
||||
if not settings.keep_power_mode:
|
||||
ht.send((f"{url}powerstate?newstate=0", "Toggle Standby "))
|
||||
@@ -434,21 +499,21 @@ def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_
|
||||
else:
|
||||
if download_type is not DownloadType.PICONS:
|
||||
# Telnet
|
||||
tn = telnet(host=host, user=user, password=password, timeout=settings.telnet_timeout)
|
||||
tn = telnet(host=host, port=telnet_port, user=user, password=password, timeout=settings.telnet_timeout)
|
||||
next(tn)
|
||||
# Terminate Enigma2 or Neutrino.
|
||||
callback("Telnet initialization ...")
|
||||
tn.send("init 4")
|
||||
callback("Stopping GUI...")
|
||||
|
||||
with UtfFTP(host=host, user=user, passwd=password) as ftp:
|
||||
with UtfFTP(host=host, port=ftp_port, user=user, passwd=password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
callback("FTP OK.")
|
||||
sat_xml_path = settings.satellites_xml_path
|
||||
services_path = settings.services_path
|
||||
|
||||
if download_type is DownloadType.SATELLITES:
|
||||
ftp.upload_xml(data_path, sat_xml_path, STC_XML_FILE, callback)
|
||||
ftp.upload_xml(data_path, sat_xml_path, files_filter or STC_XML_FILE, callback)
|
||||
|
||||
if s_type is SettingsType.NEUTRINO_MP and download_type is DownloadType.WEBTV:
|
||||
ftp.upload_xml(data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
|
||||
@@ -457,8 +522,10 @@ def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_
|
||||
ftp.cwd(services_path)
|
||||
ftp.upload_bouquets(data_path, settings.remove_unused_bouquets, callback)
|
||||
|
||||
if download_type is DownloadType.ALL:
|
||||
ftp.upload_xml(data_path, sat_xml_path, STC_XML_FILE, callback)
|
||||
if download_type is DownloadType.ALL or download_type is DownloadType.SERVICES:
|
||||
if download_type is DownloadType.ALL:
|
||||
ftp.upload_xml(data_path, sat_xml_path, files_filter or STC_XML_FILE, callback)
|
||||
|
||||
if s_type is SettingsType.NEUTRINO_MP:
|
||||
ftp.upload_xml(data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
|
||||
|
||||
@@ -497,7 +564,8 @@ def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_
|
||||
if compress:
|
||||
if not tn:
|
||||
callback("Telnet initialization...")
|
||||
tn = telnet(host=host, user=user, password=password, timeout=settings.telnet_timeout)
|
||||
tn = telnet(host=host, port=telnet_port, user=user, password=password,
|
||||
timeout=settings.telnet_timeout)
|
||||
next(tn)
|
||||
|
||||
callback("Extracting...")
|
||||
@@ -518,10 +586,14 @@ def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
ht.send((f"{url}servicelistreload?mode=2", "Reloading Userbouquets."))
|
||||
elif download_type is DownloadType.ALL:
|
||||
elif download_type is DownloadType.ALL or download_type is DownloadType.SERVICES:
|
||||
ht.send((f"{url}servicelistreload?mode=0", "Reloading lamedb and Userbouquets."))
|
||||
time.sleep(2)
|
||||
ht.send((f"{url}servicelistreload?mode=4", "Updating parental control."))
|
||||
if not settings.keep_power_mode:
|
||||
ht.send((f"{url}powerstate?newstate=4", "Wakeup from Standby."))
|
||||
elif download_type is DownloadType.SATELLITES:
|
||||
ht.send((f"{url}servicelistreload?mode=3", "Reloading transponders."))
|
||||
else:
|
||||
ht.send((f"{url}reloadchannels", "Reloading channels..."))
|
||||
|
||||
@@ -537,10 +609,12 @@ def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_
|
||||
def get_upload_info_message(download_type):
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
return "User bouquets will be updated!"
|
||||
if download_type is DownloadType.SERVICES:
|
||||
return "User bouquets and services list will be updated!"
|
||||
elif download_type is DownloadType.ALL:
|
||||
return "All user data will be reloaded!"
|
||||
elif download_type is DownloadType.SATELLITES:
|
||||
return "Satellites.xml file will be updated!"
|
||||
return "*.xml file will be updated!"
|
||||
elif download_type is DownloadType.PICONS:
|
||||
return "Picons will be updated!"
|
||||
return ""
|
||||
@@ -576,7 +650,7 @@ def http(user, password, url, callback, use_ssl=False, s_type=SettingsType.ENIGM
|
||||
|
||||
def telnet(host, port=23, user="", password="", timeout=5):
|
||||
try:
|
||||
tn = Telnet(host=host, port=port, timeout=timeout)
|
||||
tn = ExtTelnet(host=host, port=port, timeout=timeout)
|
||||
except socket.timeout:
|
||||
log("telnet error: socket timeout")
|
||||
else:
|
||||
@@ -880,7 +954,7 @@ class HttpAPI:
|
||||
|
||||
def test_ftp(host, port, user, password, timeout=5):
|
||||
try:
|
||||
with FTP(host=host, user=user, passwd=password, timeout=timeout) as ftp:
|
||||
with UtfFTP(host=host, port=port, user=user, passwd=password, timeout=timeout) as ftp:
|
||||
return ftp.getwelcome()
|
||||
except all_errors as e:
|
||||
raise TestException(e)
|
||||
@@ -927,7 +1001,7 @@ def test_telnet(host, port, user, password, timeout=5):
|
||||
|
||||
|
||||
def telnet_test(host, port, user, password, timeout):
|
||||
tn = Telnet(host=host, port=port, timeout=timeout)
|
||||
tn = ExtTelnet(host=host, port=port, timeout=timeout)
|
||||
time.sleep(1)
|
||||
tn.read_until(b"login: ", timeout=2)
|
||||
tn.write(user.encode("utf-8") + b"\r")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2025 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
|
||||
@@ -29,7 +29,7 @@ from app.commons import run_task
|
||||
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 to_bouquet_id, BouquetsWriter, BouquetsReader
|
||||
from .enigma.bouquets import BouquetsWriter, BouquetsReader
|
||||
from .enigma.lamedb import get_services as get_enigma_services, write_services as write_enigma_services
|
||||
from .iptv import parse_m3u
|
||||
from .neutrino.bouquets import get_bouquets as get_neutrino_bouquets, write_bouquets as write_neutrino_bouquets
|
||||
@@ -38,10 +38,9 @@ from .satxml import get_satellites, write_satellites
|
||||
|
||||
|
||||
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 s_type is SettingsType.NEUTRINO_MP:
|
||||
if s_type is SettingsType.NEUTRINO_MP:
|
||||
return get_neutrino_services(data_path)
|
||||
return get_enigma_services(data_path, format_version)
|
||||
|
||||
|
||||
@run_task
|
||||
@@ -53,10 +52,11 @@ def write_services(path, channels, s_type, format_version):
|
||||
|
||||
|
||||
def get_bouquets(path, s_type):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
return BouquetsReader(path).get()
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
return get_neutrino_bouquets(path)
|
||||
if s_type is SettingsType.NEUTRINO_MP:
|
||||
return get_neutrino_bouquets(path), 0
|
||||
|
||||
reader = BouquetsReader(path)
|
||||
return reader.get(), reader.errors
|
||||
|
||||
|
||||
def write_bouquet(path, bq, s_type):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2025 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
|
||||
@@ -64,7 +64,7 @@ Terrestrial = namedtuple("Terrestrial", ["name", "flags", "countrycode", "transp
|
||||
Cable = namedtuple("Cable", ["name", "flags", "satfeed", "countrycode", "transponders"])
|
||||
|
||||
Transponder = namedtuple("Transponder", ["frequency", "symbol_rate", "polarization", "fec_inner", "system",
|
||||
"modulation", "pls_mode", "pls_code", "is_id", "t2mi_plp_id"])
|
||||
"modulation", "pls_mode", "pls_code", "is_id", "t2mi_plp_id", "t2mi_pid"])
|
||||
TerTransponder = namedtuple("TerTransponder", ["centre_frequency", "system", "bandwidth", "constellation",
|
||||
"code_rate_hp", "code_rate_lp", "guard_interval", "transmission_mode",
|
||||
"hierarchy_information", "inversion", "plp_id"])
|
||||
@@ -72,12 +72,16 @@ CableTransponder = namedtuple("CableTransponder", ["frequency", "symbol_rate", "
|
||||
|
||||
|
||||
class TrType(Enum):
|
||||
""" Transponders type """
|
||||
""" Transponders type. """
|
||||
Satellite = "s"
|
||||
Terrestrial = "t"
|
||||
Cable = "c"
|
||||
ATSC = "a"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.Satellite
|
||||
|
||||
|
||||
class BqType(Enum):
|
||||
""" Bouquet type. """
|
||||
@@ -225,7 +229,8 @@ A_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM
|
||||
|
||||
# CAS
|
||||
CAS = {"C:26": "BISS", "C:0B": "Conax", "C:06": "Irdeto", "C:18": "Nagravision", "C:05": "Viaccess", "C:01": "SECA",
|
||||
"C:0E": "PowerVu", "C:4A": "DRE-Crypt", "C:7B": "DRE-Crypt", "C:56": "Verimatrix", "C:09": "VideoGuard"}
|
||||
"C:0E": "PowerVu", "C:4A": "DRE-Crypt", "C:7B": "DRE-Crypt", "C:56": "Verimatrix", "C:09": "VideoGuard",
|
||||
"C:4AFC": "Panaccess"}
|
||||
|
||||
# 'on' attribute 0070(hex) = 112(int) = ONID(ONID-TID on www.lyngsat.com)
|
||||
PROVIDER = {112: "HTB+", 253: "Tricolor TV"}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2025 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
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
|
||||
""" Module for working with Enigma2 bouquets. """
|
||||
import os.path
|
||||
import re
|
||||
from collections import Counter
|
||||
from enum import Enum
|
||||
@@ -41,6 +42,24 @@ _DEFAULT_BOUQUET_NAME = "favourites"
|
||||
_MARKER_PREFIX = "[MARKER!] "
|
||||
|
||||
|
||||
class ServiceType(Enum):
|
||||
SERVICE = "0"
|
||||
BOUQUET = "7" # Sub bouquet.
|
||||
MARKER = "64"
|
||||
SPACE = "832"
|
||||
ALT = "134" # Alternatives.
|
||||
UDP = "256"
|
||||
HIDDEN = "519" # Skip, hide.
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
log("Error. No matching service type [{} {}] was found.".format(cls.__name__, value))
|
||||
return cls.SERVICE
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class BouquetsWriter:
|
||||
""" Class for creating and writing bouquet files.
|
||||
|
||||
@@ -138,11 +157,10 @@ class BouquetsWriter:
|
||||
bouquet.append(self._ALT.format(f_name))
|
||||
self.write_bouquet(f"{p.parent}/{f_name}", srv.service, services)
|
||||
else:
|
||||
data = to_bouquet_id(srv)
|
||||
if srv.service:
|
||||
bouquet.append(f"#SERVICE {data}:{srv.service}\n#DESCRIPTION {srv.service}\n")
|
||||
bouquet.append(f"#SERVICE {srv.fav_id}:{srv.service}\n#DESCRIPTION {srv.service}\n")
|
||||
else:
|
||||
bouquet.append(f"#SERVICE {data}\n")
|
||||
bouquet.append(f"#SERVICE {srv.fav_id}\n")
|
||||
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(bouquet)
|
||||
@@ -160,34 +178,22 @@ class BouquetsWriter:
|
||||
file.writelines(bouquet)
|
||||
|
||||
|
||||
class ServiceType(Enum):
|
||||
SERVICE = "0"
|
||||
BOUQUET = "7" # Sub bouquet.
|
||||
MARKER = "64"
|
||||
SPACE = "832"
|
||||
ALT = "134" # Alternatives.
|
||||
UDP = "256"
|
||||
HIDDEN = "519" # Skip, hide.
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
log("Error. No matching service type [{} {}] was found.".format(cls.__name__, value))
|
||||
return cls.SERVICE
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class BouquetsReader:
|
||||
""" Class for reading and parsing bouquets. """
|
||||
_BQ_PAT = re.compile(r".*FROM BOUQUET\s+\"((.*bouquet|alternatives)?\.?([\w-]+)\.?(\w+)?)\"\s+.*$", re.IGNORECASE)
|
||||
_BQ_PAT2 = re.compile(r"#SERVICE:+\s+(?:[0-9a-f]+:+)+([^:]+[.](?:tv|radio))$", re.IGNORECASE)
|
||||
_BQ_POST_PAT = re.compile(r".*FROM BOUQUET\s+\"((.*bouquet|alternatives)?\.?(.*)\.?(\w+)?)\"\s+.*$", re.IGNORECASE)
|
||||
_STREAM_TYPES = {"4097", "5001", "5002", "8193", "8739"}
|
||||
|
||||
__slots__ = ["_path"]
|
||||
__slots__ = ["_path", "_errors"]
|
||||
|
||||
def __init__(self, path):
|
||||
def __init__(self, path=""):
|
||||
self._path = path
|
||||
self._errors = 0
|
||||
|
||||
@property
|
||||
def errors(self):
|
||||
return self._errors
|
||||
|
||||
def get(self):
|
||||
""" Returns a tuple of TV and Radio bouquets. """
|
||||
@@ -199,6 +205,7 @@ class BouquetsReader:
|
||||
_, _, bqs_name = line.partition("#NAME")
|
||||
if not bqs_name:
|
||||
log(f"No bouquets name found in '{bq_name}'")
|
||||
self._errors += 1
|
||||
bqs_name = "Bouquets (TV)" if bq_type == BqType.TV.value else "Bouquets (Radio)"
|
||||
bouquets = Bouquets(bqs_name.strip(), bq_type, [])
|
||||
|
||||
@@ -207,9 +214,10 @@ class BouquetsReader:
|
||||
|
||||
for line in file.readlines():
|
||||
if "#SERVICE" in line:
|
||||
mt = re.match(self._BQ_PAT, line)
|
||||
s_data = line.split(":")
|
||||
s_type = ServiceType(s_data[1])
|
||||
s_type = ServiceType.BOUQUET
|
||||
|
||||
mt = re.match(self._BQ_PAT, line) or re.match(self._BQ_PAT2, line)
|
||||
if not mt:
|
||||
# Additional file name checking.
|
||||
mt = re.match(self._BQ_POST_PAT, line)
|
||||
@@ -217,7 +225,14 @@ class BouquetsReader:
|
||||
log(f"Warning: The bouquet file name may be formed incorrectly. -> {mt.group(1)}")
|
||||
|
||||
if mt:
|
||||
file_name, prefix, b_name = mt.group(1), mt.group(2), mt.group(3)
|
||||
if len(mt.groups()) > 1:
|
||||
file_name, prefix, b_name = mt.group(1), mt.group(2), mt.group(3)
|
||||
s_type = ServiceType(s_data[1])
|
||||
s_data[:2] = "10"
|
||||
else:
|
||||
file_name, prefix, b_name = mt.group(1), "", ""
|
||||
s_type = ServiceType(s_data[2])
|
||||
|
||||
if b_name in b_names:
|
||||
log(f"The list of bouquets contains duplicate [{b_name}] names!")
|
||||
else:
|
||||
@@ -231,7 +246,6 @@ class BouquetsReader:
|
||||
else:
|
||||
real_b_names[rb_name] = 0
|
||||
# Locked, hidden.
|
||||
s_data[:2] = "10"
|
||||
locked = ":".join(s_data).rstrip()
|
||||
hidden = s_type is ServiceType.HIDDEN
|
||||
bouquets[2].append(Bouquet(rb_name, bq_type, services, locked, hidden, file_name))
|
||||
@@ -241,21 +255,30 @@ class BouquetsReader:
|
||||
bouquets[2].append(Bouquet(b_name, BqType.MARKER.value, [], None, None, line.strip()))
|
||||
else:
|
||||
log(f"Unsupported or invalid data format: [{line}].")
|
||||
self._errors += 1
|
||||
else:
|
||||
log(f"Unsupported or invalid line format: [{line}].")
|
||||
self._errors += 1
|
||||
|
||||
return bouquets
|
||||
|
||||
@staticmethod
|
||||
def get_bouquet(path, f_name, bq_name):
|
||||
def get_bouquet(self, path, f_name, bq_name):
|
||||
""" Parsing services ids from bouquet file. """
|
||||
with open(f"{path}{f_name}", encoding="utf-8", errors="replace") as file:
|
||||
bq_file = f"{path}{f_name}"
|
||||
services = []
|
||||
|
||||
if not os.path.isfile(bq_file):
|
||||
log(f"Bouquet reading error: No such bouquet [{bq_name}] file -> '{f_name}'.")
|
||||
self._errors += 1
|
||||
return f"! -> {bq_name}", services
|
||||
|
||||
with open(bq_file, encoding="utf-8", errors="replace") as file:
|
||||
chs_list = file.read()
|
||||
services = []
|
||||
srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering ['']
|
||||
# May come across empty[wrong] files!
|
||||
if not srvs:
|
||||
log(f"Bouquet file '{f_name}' is empty or wrong!")
|
||||
self._errors += 1
|
||||
return f"{bq_name} [empty]", services
|
||||
|
||||
bq_name = srvs.pop(0)
|
||||
@@ -265,50 +288,43 @@ class BouquetsReader:
|
||||
data_len = len(srv_data)
|
||||
if data_len < 10:
|
||||
log(f"The bouquet [{bq_name}] service [{num}] has the wrong data format: [{srv}]")
|
||||
self._errors += 1
|
||||
continue
|
||||
|
||||
s_type = ServiceType(srv_data[1])
|
||||
if s_type is ServiceType.MARKER:
|
||||
m_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num))
|
||||
m_data, sep, desc = srv_data[-1].partition("#DESCRIPTION")
|
||||
services.append(BouquetService(desc.strip() if desc else m_data, BqServiceType.MARKER, srv, num))
|
||||
elif s_type is ServiceType.SPACE:
|
||||
m_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
|
||||
elif s_type is ServiceType.ALT:
|
||||
alt = re.match(BouquetsReader._BQ_PAT, srv)
|
||||
alt = re.match(self._BQ_PAT, srv)
|
||||
if alt:
|
||||
af_name, alt_name = alt.group(1), alt.group(3)
|
||||
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, af_name, alt_name)
|
||||
alt_bq_name, alt_srvs = self.get_bouquet(path, af_name, alt_name)
|
||||
services.append(BouquetService(alt_bq_name, BqServiceType.ALT, alt_name, tuple(alt_srvs)))
|
||||
elif s_type is ServiceType.BOUQUET:
|
||||
sub = re.match(BouquetsReader._BQ_PAT, srv)
|
||||
sub = re.match(self._BQ_PAT, srv)
|
||||
if sub:
|
||||
sf_name, sub_name, sub_type = sub.group(1), sub.group(3), sub.group(4)
|
||||
sub_bq_name, sub_srvs = BouquetsReader.get_bouquet(path, sf_name, sub_name)
|
||||
sub_bq_name, sub_srvs = self.get_bouquet(path, sf_name, sub_name)
|
||||
bq = Bouquet(sub_bq_name, sub_type, tuple(sub_srvs), None, None, sf_name)
|
||||
services.append(BouquetService(sub_bq_name, BqServiceType.BOUQUET, bq, num))
|
||||
elif srv_data[0].strip() in BouquetsReader._STREAM_TYPES or srv_data[10].startswith(("http", "rtsp")):
|
||||
elif srv_data[0].strip() in self._STREAM_TYPES or srv_data[10].startswith(("http", "rtsp")):
|
||||
stream_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip()
|
||||
services.append(BouquetService(desc, BqServiceType.IPTV, srv, num))
|
||||
else:
|
||||
fav_id = f"{srv_data[3]}:{srv_data[4]}:{srv_data[5]}:{srv_data[6]}"
|
||||
fav_id = srv.strip().upper()
|
||||
name = None
|
||||
if data_len == 12:
|
||||
fav_id = f":".join(srv_data[:11])
|
||||
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")
|
||||
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num))
|
||||
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id, num))
|
||||
|
||||
return bq_name.lstrip("#NAME").strip(), services
|
||||
|
||||
|
||||
def to_bouquet_id(srv):
|
||||
""" Creates bouquet channel id. """
|
||||
data_type = srv.data_id
|
||||
if data_type and len(data_type) > 4:
|
||||
data_type = int(srv.data_id.split(":")[4])
|
||||
|
||||
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, srv.fav_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2024 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
|
||||
@@ -119,7 +119,10 @@ class LameDbReader:
|
||||
srv_data[1] = srv_data[1].strip("\"\n")
|
||||
data_len = len(srv_data)
|
||||
if data_len == 3:
|
||||
srv_data[2] = srv_data[2].strip()
|
||||
s_data = srv_data[2].strip()
|
||||
if not s_data.startswith("p:"):
|
||||
s_data = f"p:,{s_data}"
|
||||
srv_data[2] = s_data
|
||||
elif data_len == 2:
|
||||
srv_data.append("p:")
|
||||
srvs.extend(srv_data)
|
||||
@@ -168,15 +171,16 @@ class LameDbReader:
|
||||
ssid = str(data[0]).lstrip(sp).upper()
|
||||
onid = str(data[1]).lstrip(sp).upper()
|
||||
# For comparison in bouquets. Needed in upper case!!!
|
||||
fav_id = f"{ssid}:{tid}:{nid}:{onid}"
|
||||
fav_id = f"1:0:{srv_type:X}:{ssid}:{tid}:{nid}:{onid}:0:0:0:"
|
||||
if len(data) > 9:
|
||||
fav_id = f"{fav_id}:0:0:0:0"
|
||||
picon_id = f"1_0_{srv_type:X}_{ssid}_{tid}_{nid}_{onid}_0_0_0.png"
|
||||
s_id = f"1:0:{srv_type:X}:{ssid}:{tid}:{nid}:{onid}:0:0:0:"
|
||||
|
||||
all_flags = srv[2].split(",")
|
||||
coded = CODED_ICON if list(filter(lambda x: x.startswith("C:"), all_flags)) else None
|
||||
flags = list(filter(lambda x: x.startswith("f:"), all_flags))
|
||||
hide = HIDE_ICON if flags and Flag.is_hide(Flag.parse(flags[0])) else None
|
||||
locked = LOCKED_ICON if s_id in blacklist else None
|
||||
locked = LOCKED_ICON if fav_id in blacklist else None
|
||||
|
||||
package = list(filter(lambda x: x.startswith("p:"), all_flags))
|
||||
package = package[0][2:] if package else ""
|
||||
@@ -289,7 +293,8 @@ class LameDbReader:
|
||||
i += 1
|
||||
tmp.append(line)
|
||||
if i == size:
|
||||
if not line.startswith("p:"):
|
||||
# check if provider (p:) is present in line
|
||||
if "p:" not in line:
|
||||
# To prevent cases of incorrect service data formation
|
||||
# (e.g. the name contains a line break)
|
||||
tmp.pop()
|
||||
|
||||
78
app/eparser/enigma/streamrelay.py
Normal file
78
app/eparser/enigma/streamrelay.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2024 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
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Additional module to use stream relay functionality.
|
||||
|
||||
Reads/Writes 'whitelist_streamrelay' file.
|
||||
"""
|
||||
import os.path
|
||||
from contextlib import suppress
|
||||
|
||||
from app.commons import log
|
||||
|
||||
_FILE_NAME = "whitelist_streamrelay"
|
||||
|
||||
|
||||
class StreamRelay(dict):
|
||||
""" Class to hold/process service references used by a stream relay. """
|
||||
|
||||
def refresh(self, path):
|
||||
self.clear()
|
||||
f_path = f"{path}{_FILE_NAME}"
|
||||
if os.path.isfile(f_path):
|
||||
log("Updating stream relay cache...")
|
||||
with suppress(FileNotFoundError):
|
||||
with open(f"{path}{_FILE_NAME}", "r", encoding="utf-8") as file:
|
||||
refs = filter(None, (x.rstrip("\n") for x in file.readlines()))
|
||||
self.update(self.get_ref_data(ref) for ref in refs)
|
||||
|
||||
def get_ref_data(self, ref):
|
||||
""" Returns tuple from FAV ID and ref or ref and None for comments. """
|
||||
data = ref.split(":")
|
||||
if len(data) == 11:
|
||||
if "http" in data[-1]:
|
||||
return ref.replace("%3a", "%3A"), ref
|
||||
return f"{data[3]}:{data[4]}:{data[5]}:{data[6]}", ref
|
||||
return ref, None
|
||||
|
||||
def save(self, path):
|
||||
""" Saves current refs to a file.
|
||||
|
||||
If no refs is present, delites current relay file.
|
||||
"""
|
||||
f_name = f"{path}{_FILE_NAME}"
|
||||
if len(self):
|
||||
with open(f_name, "w", encoding="utf-8") as file:
|
||||
file.writelines([f"{v if v else k}\n\n" for k, v in self.items()])
|
||||
else:
|
||||
if os.path.exists(f_name):
|
||||
os.remove(f_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2025 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
|
||||
@@ -42,6 +42,8 @@ ENIGMA2_FAV_ID_FORMAT = " {}:{}:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTI
|
||||
MARKER_FORMAT = " 1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n"
|
||||
PICON_FORMAT = "{}_{}_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png"
|
||||
|
||||
ENCODING_BLACKLIST = {"MacRoman"}
|
||||
|
||||
|
||||
class StreamType(Enum):
|
||||
DVB_TS = "1"
|
||||
@@ -73,6 +75,7 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
|
||||
else:
|
||||
enc = chardet.detect(data)
|
||||
encoding = enc.get("encoding", "utf-8")
|
||||
encoding = "utf-8" if encoding in ENCODING_BLACKLIST else encoding
|
||||
|
||||
aggr = [None] * 10
|
||||
s_aggr = aggr[: -3]
|
||||
@@ -99,6 +102,7 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
|
||||
data = dict(pattern.findall(line))
|
||||
name = data.get("tvg-name", name)
|
||||
picon = data.get("tvg-logo", None)
|
||||
epg_id = data.get("tvg-id", None)
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
group = data.get("group-title", None)
|
||||
@@ -109,6 +113,7 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
|
||||
params[0] = sid_counter
|
||||
sid_counter += 1
|
||||
fav_id = get_fav_id(url, name, s_type, params)
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
p_id = get_picon_id(params)
|
||||
if group not in groups:
|
||||
@@ -120,7 +125,7 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
|
||||
services.append(Service(None, None, None, group, *aggr[0:3], m_name, *aggr, m_id, None))
|
||||
|
||||
if all((name, url, fav_id)):
|
||||
services.append(Service(None, None, IPTV_ICON, name, *aggr[0:2], group,
|
||||
services.append(Service(epg_id, None, IPTV_ICON, name, *aggr[0:2], group,
|
||||
st, picon, p_id, *s_aggr, url, fav_id, None))
|
||||
else:
|
||||
log(f"*.m3u* parse error ['{path}']: name[{name}], url[{url}], fav id[{fav_id}]")
|
||||
@@ -142,7 +147,11 @@ def export_to_m3u(path, bouquet, s_type, url=None):
|
||||
lines.append(f"#EXTINF:-1,{s.name}\n")
|
||||
lines.append(current_grp) if current_grp else None
|
||||
u = res.group(1)
|
||||
lines.append(f"{unquote(u[:u.rfind(':')]) if s_type is SettingsType.ENIGMA_2 else u}\n")
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
index = u.rfind(":")
|
||||
lines.append(f"{unquote(u[:index] if index > 0 else u)}\n")
|
||||
else:
|
||||
lines.append(f"{u}\n")
|
||||
elif srv_type is BqServiceType.MARKER:
|
||||
current_grp = f"#EXTGRP:{s.name}\n"
|
||||
elif srv_type is BqServiceType.DEFAULT and url:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2025 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
|
||||
@@ -88,7 +88,8 @@ def get_sat_transponders(elem):
|
||||
e.get("pls_mode", None),
|
||||
e.get("pls_code", None),
|
||||
e.get("is_id", None),
|
||||
e.get("t2mi_plp_id", None)) for e in elem.iter("transponder")]
|
||||
e.get("t2mi_plp_id", None),
|
||||
e.get("t2mi_pid", None)) for e in elem.iter("transponder")]
|
||||
|
||||
|
||||
def get_terrestrial(path):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2025 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
|
||||
@@ -56,9 +56,9 @@ class Defaults:
|
||||
USER = "root"
|
||||
PASSWORD = ""
|
||||
HOST = "127.0.0.1"
|
||||
FTP_PORT = "21"
|
||||
HTTP_PORT = "80"
|
||||
TELNET_PORT = "23"
|
||||
FTP_PORT = 21
|
||||
HTTP_PORT = 80
|
||||
TELNET_PORT = 23
|
||||
HTTP_USE_SSL = False
|
||||
# Enigma2.
|
||||
BOX_SERVICES_PATH = "/etc/enigma2/"
|
||||
@@ -305,11 +305,11 @@ class Settings:
|
||||
self._cp_settings["hosts"] = value
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
return self._cp_settings.get("port", self.get_default("port"))
|
||||
def port(self) -> int:
|
||||
return int(self._cp_settings.get("port", self.get_default("port")))
|
||||
|
||||
@port.setter
|
||||
def port(self, value):
|
||||
def port(self, value: int):
|
||||
self._cp_settings["port"] = value
|
||||
|
||||
@property
|
||||
@@ -329,19 +329,19 @@ class Settings:
|
||||
self._cp_settings["password"] = value
|
||||
|
||||
@property
|
||||
def http_port(self):
|
||||
return self._cp_settings.get("http_port", self.get_default("http_port"))
|
||||
def http_port(self) -> int:
|
||||
return int(self._cp_settings.get("http_port", self.get_default("http_port")))
|
||||
|
||||
@http_port.setter
|
||||
def http_port(self, value):
|
||||
def http_port(self, value: int):
|
||||
self._cp_settings["http_port"] = value
|
||||
|
||||
@property
|
||||
def http_timeout(self):
|
||||
def http_timeout(self) -> int:
|
||||
return self._cp_settings.get("http_timeout", self.get_default("http_timeout"))
|
||||
|
||||
@http_timeout.setter
|
||||
def http_timeout(self, value):
|
||||
def http_timeout(self, value: int):
|
||||
self._cp_settings["http_timeout"] = value
|
||||
|
||||
@property
|
||||
@@ -353,11 +353,11 @@ class Settings:
|
||||
self._cp_settings["http_use_ssl"] = value
|
||||
|
||||
@property
|
||||
def telnet_port(self):
|
||||
return self._cp_settings.get("telnet_port", self.get_default("telnet_port"))
|
||||
def telnet_port(self) -> int:
|
||||
return int(self._cp_settings.get("telnet_port", self.get_default("telnet_port")))
|
||||
|
||||
@telnet_port.setter
|
||||
def telnet_port(self, value):
|
||||
def telnet_port(self, value: int):
|
||||
self._cp_settings["telnet_port"] = value
|
||||
|
||||
@property
|
||||
@@ -604,6 +604,15 @@ class Settings:
|
||||
def epg_xml_sources(self, value):
|
||||
self._cp_settings["epg_xml_sources"] = value
|
||||
|
||||
@property
|
||||
def enable_epg_name_cache(self):
|
||||
""" Enables additional name cache for EPG. """
|
||||
return self._settings.get("enable_epg_name_cache", False)
|
||||
|
||||
@enable_epg_name_cache.setter
|
||||
def enable_epg_name_cache(self, value):
|
||||
self._settings["enable_epg_name_cache"] = value
|
||||
|
||||
# *********** FTP ************ #
|
||||
|
||||
@property
|
||||
|
||||
106
app/tools/epg.py
106
app/tools/epg.py
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -32,7 +32,6 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
import struct
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import namedtuple, defaultdict
|
||||
from datetime import datetime, timezone
|
||||
@@ -258,61 +257,64 @@ class XmlTvReader(Reader):
|
||||
log(f"{self.__class__.__name__} [download] error: Invalid URL {self._url}")
|
||||
return
|
||||
|
||||
with requests.get(url=self._url, stream=True) as resp:
|
||||
if resp.reason == "OK":
|
||||
suf = self._url[self._url.rfind("."):]
|
||||
if suf not in self.SUFFIXES:
|
||||
log(f"{self.__class__.__name__} [download] error: Unsupported file extension.")
|
||||
return
|
||||
try:
|
||||
with requests.get(url=self._url, stream=True, timeout=(5, 5)) as resp:
|
||||
if resp.reason == "OK":
|
||||
suf = self._url[self._url.rfind("."):]
|
||||
if suf not in self.SUFFIXES:
|
||||
log(f"{self.__class__.__name__} [download] error: Unsupported file extension.")
|
||||
return
|
||||
|
||||
data_size = resp.headers.get("content-length")
|
||||
if not data_size:
|
||||
log(f"{self.__class__.__name__} [download *.{suf}] error: Error getting data size.")
|
||||
if clb:
|
||||
clb()
|
||||
return
|
||||
data_size = resp.headers.get("content-length")
|
||||
if not data_size:
|
||||
log(f"{self.__class__.__name__} [download *.{suf}] error: Error getting data size.")
|
||||
return
|
||||
|
||||
with NamedTemporaryFile(suffix=suf, delete=not IS_WIN) as tf:
|
||||
downloaded = 0
|
||||
data_size = int(data_size)
|
||||
log("Downloading XMLTV file...")
|
||||
for data in resp.iter_content(chunk_size=1024):
|
||||
downloaded += len(data)
|
||||
tf.write(data)
|
||||
done = int(50 * downloaded / data_size)
|
||||
sys.stdout.write(f"\rDownloading XMLTV file [{'=' * done}{' ' * (50 - done)}]")
|
||||
sys.stdout.flush()
|
||||
tf.seek(0)
|
||||
sys.stdout.write("\n")
|
||||
with NamedTemporaryFile(suffix=suf, delete=not IS_WIN) as tf:
|
||||
downloaded = 0
|
||||
data_size = int(data_size)
|
||||
completed = set()
|
||||
|
||||
os.makedirs(os.path.dirname(self._path), exist_ok=True)
|
||||
for data in resp.iter_content(chunk_size=128):
|
||||
downloaded += len(data)
|
||||
tf.write(data)
|
||||
done = int(100 * downloaded / data_size)
|
||||
if done % 25 == 0 and done not in completed:
|
||||
completed.add(done)
|
||||
log(f"Downloading XMLTV file...{done}%" if done < 100 else "XMLTV file download complete.")
|
||||
tf.seek(0)
|
||||
|
||||
if suf.endswith(".gz"):
|
||||
try:
|
||||
shutil.copyfile(tf.name, self._path)
|
||||
except OSError as e:
|
||||
log(f"{self.__class__.__name__} [download *.gz] error: {e}")
|
||||
elif self._url.endswith((".xz", ".lzma")):
|
||||
import lzma
|
||||
os.makedirs(os.path.dirname(self._path), exist_ok=True)
|
||||
|
||||
try:
|
||||
with lzma.open(tf, "rb") as lzf:
|
||||
shutil.copyfileobj(lzf, self._path)
|
||||
except (lzma.LZMAError, OSError) as e:
|
||||
log(f"{self.__class__.__name__} [download *.xz] error: {e}")
|
||||
else:
|
||||
try:
|
||||
import gzip
|
||||
with gzip.open(self._path, "wb") as f_out:
|
||||
shutil.copyfileobj(tf, f_out)
|
||||
except OSError as e:
|
||||
log(f"{self.__class__.__name__} [download *.xml] error: {e}")
|
||||
if suf.endswith(".gz"):
|
||||
try:
|
||||
shutil.copyfile(tf.name, self._path)
|
||||
except OSError as e:
|
||||
log(f"{self.__class__.__name__} [download *.gz] error: {e}")
|
||||
elif self._url.endswith((".xz", ".lzma")):
|
||||
import lzma
|
||||
|
||||
if IS_WIN and os.path.isfile(tf.name):
|
||||
tf.close()
|
||||
os.remove(tf.name)
|
||||
else:
|
||||
log(f"{self.__class__.__name__} [download] error: {resp.reason}")
|
||||
try:
|
||||
with lzma.open(tf, "rb") as lzf:
|
||||
shutil.copyfileobj(lzf, self._path)
|
||||
except (lzma.LZMAError, OSError) as e:
|
||||
log(f"{self.__class__.__name__} [download *.xz] error: {e}")
|
||||
else:
|
||||
try:
|
||||
import gzip
|
||||
with gzip.open(self._path, "wb") as f_out:
|
||||
shutil.copyfileobj(tf, f_out)
|
||||
except OSError as e:
|
||||
log(f"{self.__class__.__name__} [download *.xml] error: {e}")
|
||||
|
||||
if IS_WIN and os.path.isfile(tf.name):
|
||||
tf.close()
|
||||
os.remove(tf.name)
|
||||
else:
|
||||
log(f"{self.__class__.__name__} [download] error: {resp.reason}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
log(f"{self.__class__.__name__} [download] error: {e}")
|
||||
return
|
||||
|
||||
if clb:
|
||||
clb()
|
||||
@@ -324,7 +326,7 @@ class XmlTvReader(Reader):
|
||||
utc = dt.timestamp()
|
||||
offset = datetime.now() - dt
|
||||
|
||||
for srv in filter(lambda s: any(name in names for name in s.names), self._cache.values()):
|
||||
for srv in filter(lambda s: s.id in names or any(name in names for name in s.names), self._cache.values()):
|
||||
[self.process_event(ev, events, offset, srv) for ev in filter(lambda s: s.duration > utc, srv.events)]
|
||||
|
||||
return events
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -31,14 +31,19 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from html.parser import HTMLParser
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
from app.commons import run_task, log
|
||||
from app.commons import log, run_task
|
||||
from app.settings import SettingsType, IS_LINUX, IS_WIN, IS_DARWIN, GTK_PATH
|
||||
from .satellites import _HEADERS
|
||||
from app.tools.satellites import _HEADERS
|
||||
|
||||
_ENIGMA2_PICON_KEY = "{:X}:{:X}:{}"
|
||||
_NEUTRINO_PICON_KEY = "{:x}{:04x}{:04x}.png"
|
||||
@@ -51,6 +56,12 @@ class PiconsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PiconFormat(IntEnum):
|
||||
ENIGMA2 = 0
|
||||
NEUTRINO = 1
|
||||
OSCAM = 3
|
||||
|
||||
|
||||
class PiconsCzDownloader:
|
||||
""" The main class for loading picons from the https://picon.cz/ source (by Chocholoušek). """
|
||||
|
||||
@@ -67,46 +78,76 @@ class PiconsCzDownloader:
|
||||
self._provider_logos = {}
|
||||
self._picon_ids = picon_ids
|
||||
self._appender = appender
|
||||
self._logo_map = self.get_logos_map()
|
||||
self._name_map = self.get_name_map()
|
||||
self._perm_cache_file = Path(tempfile.gettempdir()).joinpath("picon_cz_links")
|
||||
# subprocess creation flags
|
||||
self._sbp_flags = subprocess.CREATE_NO_WINDOW if IS_WIN else 0
|
||||
|
||||
@property
|
||||
def providers(self):
|
||||
return self._providers
|
||||
|
||||
def init(self):
|
||||
""" Initializes dict with values: download_id -> perm link and provider data. """
|
||||
if self._perm_links:
|
||||
return
|
||||
|
||||
self._HEADER["Referer"] = self._PERM_URL
|
||||
if self._perm_cache_file.exists():
|
||||
st = self._perm_cache_file.stat()
|
||||
dif = datetime.now() - datetime.fromtimestamp(st.st_mtime)
|
||||
# We will update daily.
|
||||
if dif.days > 0:
|
||||
self.download_permalinks()
|
||||
else:
|
||||
self.download_permalinks()
|
||||
|
||||
self.read_permalinks()
|
||||
|
||||
def download_permalinks(self):
|
||||
self._HEADER["Referer"] = self._PERM_URL
|
||||
with requests.get(url=self._PERM_URL, headers=self._HEADER, stream=True) as request:
|
||||
if request.reason == "OK":
|
||||
logo_map = self.get_logos_map()
|
||||
name_map = self.get_name_map()
|
||||
log(f"{self.__class__.__name__}: downloading permalinks file...")
|
||||
buf = BytesIO()
|
||||
[buf.write(chunk) for chunk in request.iter_content(chunk_size=128)]
|
||||
buf.seek(0)
|
||||
|
||||
for line in request.iter_lines():
|
||||
data = line.decode(encoding="utf-8", errors="ignore").split(maxsplit=1)
|
||||
if len(data) != 2:
|
||||
continue
|
||||
|
||||
l_id, perm_link = data
|
||||
self._perm_links[str(l_id)] = str(perm_link)
|
||||
data = re.match(self._LINK_PATTERN, perm_link)
|
||||
if data:
|
||||
sat_pos = data.group(3)
|
||||
# Logo url.
|
||||
logo = logo_map.get(data.group(2), None)
|
||||
l_name = name_map.get(sat_pos, None) or sat_pos.replace(".", "")
|
||||
logo_url = f"{self._BASE_LOGO_URL}{logo}/{l_name}.png" if logo else None
|
||||
|
||||
prv = Provider(None, data.group(1), sat_pos, self._BASE_URL + l_id, l_id, logo_url, None, False)
|
||||
if sat_pos in self._providers:
|
||||
self._providers[sat_pos].append(prv)
|
||||
else:
|
||||
self._providers[sat_pos] = [prv]
|
||||
self._perm_cache_file.touch()
|
||||
self._perm_cache_file.write_bytes(buf.read())
|
||||
else:
|
||||
log(f"{self.__class__.__name__} [get permalinks] error: {request.reason}")
|
||||
raise PiconsError(request.reason)
|
||||
|
||||
@property
|
||||
def providers(self):
|
||||
return self._providers
|
||||
def read_permalinks(self):
|
||||
with self._perm_cache_file.open(encoding="utf-8", errors="ignore") as f:
|
||||
for l in f.readlines():
|
||||
data = l.split(maxsplit=1)
|
||||
if len(data) != 2:
|
||||
continue
|
||||
|
||||
data = l.split(maxsplit=1)
|
||||
if len(data) != 2:
|
||||
continue
|
||||
|
||||
l_id, perm_link = data
|
||||
self._perm_links[str(l_id)] = str(perm_link)
|
||||
self.update_provider_data(l_id, perm_link)
|
||||
|
||||
def update_provider_data(self, l_id, perm_link):
|
||||
data = re.match(self._LINK_PATTERN, perm_link)
|
||||
if data:
|
||||
sat_pos = data.group(3)
|
||||
# Logo url.
|
||||
logo = self._logo_map.get(data.group(2), None)
|
||||
l_name = self._name_map.get(sat_pos, None) or sat_pos.replace(".", "")
|
||||
logo_url = f"{self._BASE_LOGO_URL}{logo}/{l_name}.png" if logo else None
|
||||
|
||||
prv = Provider(None, data.group(1), sat_pos, self._BASE_URL + l_id, l_id, logo_url, None, False)
|
||||
if sat_pos in self._providers:
|
||||
self._providers[sat_pos].append(prv)
|
||||
else:
|
||||
self._providers[sat_pos] = [prv]
|
||||
|
||||
def get_sat_providers(self, url):
|
||||
return self._providers.get(url, [])
|
||||
@@ -142,7 +183,10 @@ class PiconsCzDownloader:
|
||||
|
||||
cmd = [exe, "l", src]
|
||||
try:
|
||||
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
out, err = subprocess.Popen(cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
creationflags=self._sbp_flags).communicate()
|
||||
if err:
|
||||
log(f"{self.__class__.__name__} [extract] error: {err}")
|
||||
raise PiconsError(err)
|
||||
@@ -167,7 +211,10 @@ class PiconsCzDownloader:
|
||||
cmd = [exe, "e", src, "-o{}".format(dest), "-y", "-r"]
|
||||
cmd.extend(to_extract)
|
||||
try:
|
||||
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
out, err = subprocess.Popen(cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
creationflags=self._sbp_flags).communicate()
|
||||
if err:
|
||||
log(f"{self.__class__.__name__} [extract] error: {err}")
|
||||
raise PiconsError(err)
|
||||
@@ -200,7 +247,8 @@ class PiconsCzDownloader:
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
log(f"{self.__class__.__name__} error [get provider logo]: {e}")
|
||||
|
||||
def get_logos_map(self):
|
||||
@staticmethod
|
||||
def get_logos_map():
|
||||
return {"piconblack": "b50",
|
||||
"picontransparent": "t50",
|
||||
"piconwhite": "w50",
|
||||
@@ -225,7 +273,8 @@ class PiconsCzDownloader:
|
||||
"piconSNPblack": "b50",
|
||||
}
|
||||
|
||||
def get_name_map(self):
|
||||
@staticmethod
|
||||
def get_name_map():
|
||||
return {"antiksat": "ANTIK",
|
||||
"digiczsk": "DIGI",
|
||||
"DTTitaly": "picon_trs-it",
|
||||
@@ -304,7 +353,7 @@ class PiconsParser(HTMLParser):
|
||||
if req.status_code == 200:
|
||||
logo_data = req.text
|
||||
else:
|
||||
log("Provider picons downloading error: {} {}".format(provider.url, req.reason))
|
||||
log(f"Provider picons downloading error: {provider.url} {req.reason}")
|
||||
return
|
||||
|
||||
on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single
|
||||
@@ -335,7 +384,7 @@ class PiconsParser(HTMLParser):
|
||||
p_name = picons_path + (name if name else os.path.basename(p.ref))
|
||||
picons_data.append(("{}{}".format(PiconsParser._BASE_URL, p.ref), p_name))
|
||||
except (TypeError, ValueError) as e:
|
||||
msg = "Picons format parse error: {}".format(p) + "\n" + str(e)
|
||||
msg = f"Picons format parse error: {p}\n{e}"
|
||||
log(msg)
|
||||
|
||||
return picons_data
|
||||
@@ -348,15 +397,15 @@ class PiconsParser(HTMLParser):
|
||||
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:
|
||||
return "{}.png".format(ssid)
|
||||
return f"{ssid}.png"
|
||||
|
||||
|
||||
class ProviderParser(HTMLParser):
|
||||
""" Parser for satellite html page. (https://www.lyngsat.com/*sat-name*.html) """
|
||||
|
||||
_POSITION_PATTERN = re.compile("at\s\d+\..*(?:E|W)']")
|
||||
_ONID_TID_PATTERN = re.compile("^\d+-\d+.*")
|
||||
_TRANSPONDER_FREQUENCY_PATTERN = re.compile("^\d+ [HVLR]+")
|
||||
_POSITION_PATTERN = re.compile(r"at\s\d+\..*(?:E|W)']")
|
||||
_ONID_TID_PATTERN = re.compile(r"^\d+-\d+.*")
|
||||
_TRANSPONDER_FREQUENCY_PATTERN = re.compile(r"^\d+ [HVLR]+")
|
||||
_DOMAINS = {"/tvchannels/", "/radiochannels/", "/packages/", "/logo/"}
|
||||
_BASE_URL = "https://www.lyngsat.com"
|
||||
|
||||
@@ -442,7 +491,7 @@ class ProviderParser(HTMLParser):
|
||||
if req.status_code == 200:
|
||||
logo_data = req.content
|
||||
else:
|
||||
log("Downloading provider logo error: {}".format(req.reason))
|
||||
log(f"Downloading provider logo error: {req.reason}")
|
||||
self.rows.append(Provider(logo=logo_data, name=name, pos=self._positon, url=row[6], on_id=on_id,
|
||||
ssid=None, single=False, selected=True))
|
||||
elif 6 < len_row < 12:
|
||||
@@ -475,7 +524,7 @@ def parse_providers(url):
|
||||
if request.status_code == 200:
|
||||
parser.feed(request.text)
|
||||
else:
|
||||
log("Parse providers error [{}]: {}".format(url, request.reason))
|
||||
log(f"Parse providers error [{url}]: {request.reason}")
|
||||
|
||||
def srt(p):
|
||||
if p.logo is None:
|
||||
@@ -504,26 +553,81 @@ def download_picon(src_url, dest_path):
|
||||
for chunk in req:
|
||||
f.write(chunk)
|
||||
except OSError as e:
|
||||
err_msg = "Saving picon [{}] error: {}".format(dest_path, e)
|
||||
err_msg = f"Saving picon [{dest_path}] error: {e}"
|
||||
log(err_msg)
|
||||
|
||||
|
||||
@run_task
|
||||
def convert_to(src_path, dest_path, s_type, done_callback):
|
||||
""" Converts names format of picons.
|
||||
def convert_to(src_path, dest_path, p_format, ids=None, services=None, done_callback=None):
|
||||
""" Converts format [names] of picons.
|
||||
|
||||
Copies resulting files from src to dest and writes state to callback.
|
||||
"""
|
||||
pattern = "/*_0_0_0.png" if s_type is SettingsType.ENIGMA_2 else "/*.png"
|
||||
pattern = "/*_0_0_0.png" if p_format is PiconFormat.NEUTRINO else "/*.png"
|
||||
to_convert = []
|
||||
for file in glob.glob(src_path + pattern):
|
||||
base_name = os.path.basename(file)
|
||||
if ids is not None and base_name not in ids:
|
||||
continue
|
||||
|
||||
to_convert.append((base_name, file))
|
||||
|
||||
if p_format is PiconFormat.NEUTRINO:
|
||||
convert_to_neutrino(to_convert, dest_path)
|
||||
elif p_format is PiconFormat.OSCAM:
|
||||
convert_to_oscam(to_convert, dest_path, services)
|
||||
|
||||
if done_callback:
|
||||
done_callback()
|
||||
|
||||
|
||||
def convert_to_neutrino(files, dest_path):
|
||||
for base_name, file in files:
|
||||
pic_data = base_name.rstrip(".png").split("_")
|
||||
dest_file = _NEUTRINO_PICON_KEY.format(int(pic_data[4], 16), int(pic_data[5], 16), int(pic_data[3], 16))
|
||||
dest = "{}/{}".format(dest_path, dest_file)
|
||||
log('Converting "{}" to "{}"'.format(base_name, dest_file))
|
||||
dest = f"{dest_path}{os.sep}{dest_file}"
|
||||
log(f'Converting "{base_name}" to "{dest_file}"')
|
||||
shutil.copyfile(file, dest)
|
||||
|
||||
done_callback()
|
||||
|
||||
def convert_to_oscam(files, dest_path, services):
|
||||
if not files:
|
||||
return
|
||||
|
||||
os.makedirs(dest_path, exist_ok=True)
|
||||
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
|
||||
for base_name, file in files:
|
||||
to_convert = []
|
||||
srv = services.get(base_name, None)
|
||||
if srv:
|
||||
sid, flags = srv.ssid, srv.flags_cas
|
||||
if flags:
|
||||
cas = list(map(lambda c: c.lstrip("C:"), filter(lambda x: x.startswith("C:"), flags.split(","))))
|
||||
if cas:
|
||||
[to_convert.append(f"{dest_path}{os.sep}IC_{c.upper()}_{sid.upper()}.tpl") for c in cas]
|
||||
else:
|
||||
to_convert.append(f"{dest_path}{os.sep}{base_name}.tpl")
|
||||
else:
|
||||
to_convert.append(f"{dest_path}{os.sep}{base_name}.tpl")
|
||||
else:
|
||||
to_convert.append(f"{dest_path}{os.sep}{base_name}.tpl")
|
||||
|
||||
image = Image.open(file)
|
||||
image.thumbnail((100, 60))
|
||||
|
||||
buff = BytesIO()
|
||||
image.save(buff, format="PNG")
|
||||
data_bytes = b"data:image/png;base64," + base64.b64encode(buff.getvalue())
|
||||
|
||||
for dest_file in to_convert:
|
||||
log(f'Converting "{base_name}" to "{dest_file}"')
|
||||
|
||||
with open(dest_file, "wb") as f:
|
||||
f.write(data_bytes)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2025 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
|
||||
@@ -338,7 +338,7 @@ class SatellitesParser(HTMLParser):
|
||||
self.FEC.get(fec, None),
|
||||
self.SYSTEM.get(sys, None),
|
||||
self.MODULATION.get(mod, None),
|
||||
pls_mode, pls_code, None, None)
|
||||
pls_mode, pls_code, None, None, None)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
@@ -379,7 +379,7 @@ class SatellitesParser(HTMLParser):
|
||||
self.FEC.get(fec, None),
|
||||
self.SYSTEM.get(sys, None),
|
||||
self.MODULATION.get(mod, None),
|
||||
pls_mode, pls_code, is_id, None)
|
||||
pls_mode, pls_code, is_id, None, None)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
@@ -392,7 +392,7 @@ class SatellitesParser(HTMLParser):
|
||||
mod_pat = re.compile(r"(.*PSK).*?(?:.*Stream\s+(\d+))?.*")
|
||||
sr_fec_pattern = re.compile(r"(\d{4,5})+\s+(\d+/\d+).*")
|
||||
|
||||
for row in filter(lambda r: len(r) == 16 and self.POS_PAT.match(r[0]), self._rows):
|
||||
for row in filter(lambda r: len(r) == 14 and self.POS_PAT.match(r[0]), self._rows):
|
||||
freq, pol = row[2].replace(".", "0"), row[3]
|
||||
if not freq.isdigit() or pol not in "VHLR":
|
||||
continue
|
||||
@@ -421,7 +421,7 @@ class SatellitesParser(HTMLParser):
|
||||
self.FEC.get(fec, None),
|
||||
self.SYSTEM.get(sys, None),
|
||||
self.MODULATION.get(mod, None),
|
||||
pls_id, pls_code, is_id, None)
|
||||
pls_id, pls_code, is_id, None, None)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
@@ -443,10 +443,8 @@ class ServicesParser(HTMLParser):
|
||||
|
||||
self._POS_PAT = re.compile(r".*?(\d+\.\d°[EW]).*")
|
||||
# LyngSat.
|
||||
self._TR_PAT = re.compile((r".*?(\d+)\.?\d?\s+([RLHV]).*(DVB-S[2]?)/?(.*PSK)?\s"
|
||||
r"?(T2-MI)?\s?(PLS\s+Multistream)?\s?"
|
||||
r"SR-FEC:\s(\d+)-(\d/\d)\s+.*ONID-TID:\s+(\d+)-(\d+).*"))
|
||||
|
||||
self._TR_PAT = re.compile(r".*?(\d{4,5})\.?\d?\s+([RLHV]).*(DVB-S2?X?)/?(.*PSK)?.*SR-FEC:\s(\d+)-(\d+/\d+).*")
|
||||
self._ID_PAT = re.compile(r"C/N lock:.*?(?:.*ONID-TID:\s+(\d+)-(\d+))?.*")
|
||||
self._MULTI_PAT = re.compile(r"PLS\s+(Root|Gold|Combo)+\s(\d+)?\s+(?:Stream\s(\d+))")
|
||||
# KingOfSat.
|
||||
self._KING_TR_PAT = re.compile((r"(DVB-S[2]?)\s?(?:T2-MI,\s+PLP\s+(\d+))?.*"
|
||||
@@ -583,9 +581,9 @@ class ServicesParser(HTMLParser):
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
trs = []
|
||||
for r in self._rows:
|
||||
if len(r) == 13 and SatellitesParser.POS_PAT.match(r[0].text):
|
||||
if len(r) == 12 and SatellitesParser.POS_PAT.match(r[0].text):
|
||||
t_cell = r[4]
|
||||
if t_cell.url and t_cell.url.startswith("tp.php?tp="):
|
||||
if t_cell.url and t_cell.url.startswith("tp"):
|
||||
t_cell.url = f"https://{self._lang}.kingofsat.tv/{t_cell.url}"
|
||||
t_cell.text = f"{r[2].text} {r[3].text} {r[6].text} {r[8].text}"
|
||||
trs.append(t_cell)
|
||||
@@ -616,13 +614,12 @@ class ServicesParser(HTMLParser):
|
||||
services = []
|
||||
pos, freq, sr, fec, pol, nsp, tid, nid = sat_position or 0, 0, 0, 0, 0, 0, 0, 0
|
||||
sys = "DVB-S"
|
||||
pos_found = False
|
||||
tr = None
|
||||
pos_found, tr, td, t_id = False, None, None, None
|
||||
# Multi-stream.
|
||||
multi_tr = None
|
||||
multi = False
|
||||
# Transponder.
|
||||
for r in filter(lambda x: x and 6 < len(x) < 9, self._rows):
|
||||
for r in self._rows:
|
||||
if not pos_found:
|
||||
pos_tr = re.match(self._POS_PAT, r[0].text)
|
||||
if not pos_tr:
|
||||
@@ -632,22 +629,23 @@ class ServicesParser(HTMLParser):
|
||||
pos = self.get_position(pos_tr.group(1))
|
||||
pos_found = True
|
||||
|
||||
if pos_found:
|
||||
text = " ".join(c.text for c in r[1:])
|
||||
td = re.match(self._TR_PAT, text)
|
||||
if td:
|
||||
if pos_found and not td:
|
||||
td = re.match(self._TR_PAT, " ".join(c.text for c in r))
|
||||
|
||||
if td and not t_id:
|
||||
t_id = re.match(self._ID_PAT, " ".join(c.text for c in r))
|
||||
if t_id:
|
||||
# The ONID-TID values may not present!
|
||||
_nid, _tid = t_id.group(1), t_id.group(2)
|
||||
if _nid and _tid:
|
||||
nid, tid = int(_nid), int(_tid)
|
||||
else:
|
||||
log((f"Values 'ONID-TID' for transponder [{self._t_url}] are not present."
|
||||
" Default values are used."))
|
||||
|
||||
freq, pol = int(td.group(1)), get_key_by_value(POLARIZATION, td.group(2))
|
||||
sys, mod, sr, _fec, = td.group(3), td.group(4), td.group(7), td.group(8)
|
||||
nid, tid = td.group(9), td.group(10)
|
||||
sys, mod, sr, _fec = td.group(3), td.group(4), td.group(5), td.group(6)
|
||||
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, _fec, sys, mod)
|
||||
nid, tid = int(nid), int(tid)
|
||||
|
||||
if td.group(5):
|
||||
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}]")
|
||||
|
||||
if td.group(6):
|
||||
log(f"Detected multi-stream transponder! [{freq} {sr} {pol}]")
|
||||
multi = True
|
||||
|
||||
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
|
||||
|
||||
@@ -683,7 +681,7 @@ class ServicesParser(HTMLParser):
|
||||
def get_kingofsat_services(self, sat_position=None, use_pids=False):
|
||||
services = []
|
||||
# Transponder
|
||||
tr = list(filter(lambda r: len(r) == 13 and r[4].url and r[4].url.startswith("tp.php?tp="), self._rows))
|
||||
tr = list(filter(lambda r: len(r) == 12 and r[4].url and r[4].url.startswith("tp"), self._rows))
|
||||
if not tr:
|
||||
log(f"ServicesParser error [get transponder services]: Transponder [{self._t_url}] not found!")
|
||||
return services
|
||||
@@ -691,9 +689,9 @@ class ServicesParser(HTMLParser):
|
||||
tr, multi_tr, tid, nid, nsp = None, None, None, None, None
|
||||
freq, sr, pol, fec, sys, pos = None, None, None, None, None, None
|
||||
|
||||
for r in filter(lambda x: len(x) > 12, self._rows):
|
||||
for r in filter(lambda x: len(x) > 11, self._rows):
|
||||
r_size = len(r)
|
||||
if r_size == 13 and r[4].url and r[4].url.startswith("tp.php?tp="):
|
||||
if r_size == 12 and r[4].url and r[4].url.startswith("tp"):
|
||||
res = re.match(self._KING_TR_PAT, f"{r[6].text} {r[7].text}")
|
||||
if not res:
|
||||
continue
|
||||
@@ -769,11 +767,12 @@ class ServicesParser(HTMLParser):
|
||||
def get_service_data(s_type, pkg, sid, tid, nid, namespace, v_pid, a_pid, cas, use_pids=False):
|
||||
sid = int(sid)
|
||||
data_id = f"{sid:04x}:{namespace}:{tid:04x}:{nid:04x}:{s_type}:0:0"
|
||||
fav_id = f"{sid}:{tid}:{nid}:{namespace}"
|
||||
fav_id = f"1:0:{int(s_type):X}:{sid}:{tid}:{nid}:{namespace}:0:0:0:"
|
||||
picon_id = f"1_0_{int(s_type):X}_{sid}_{tid}_{nid}_{namespace}_0_0_0.png"
|
||||
# Flags.
|
||||
flags = f"p:{pkg}"
|
||||
cas = ",".join(get_key_by_value(CAS, c) or "C:0000" for c in cas.split()) if cas else None
|
||||
cas = ",".join(get_key_by_value(CAS, c) or "" for c in cas.split()) if cas else None
|
||||
|
||||
if use_pids:
|
||||
v_pid = f"c:00{int(v_pid):04x}" if v_pid else None
|
||||
a_pid = ",".join([f"c:01{int(p):04x}" for p in a_pid]) if a_pid else None
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2025 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
|
||||
@@ -39,7 +39,7 @@ from urllib import parse
|
||||
from urllib.error import URLError
|
||||
from urllib.request import Request, urlopen, urlretrieve
|
||||
|
||||
from app.commons import log
|
||||
from app.commons import log, run_task
|
||||
from app.settings import SEP
|
||||
from app.ui.uicommons import show_notification
|
||||
|
||||
@@ -172,22 +172,21 @@ class InnerTube:
|
||||
_BASE_URI = "https://www.youtube.com/youtubei/v1"
|
||||
|
||||
_DEFAULT_CLIENTS = {
|
||||
"WEB_EMBED": {"context": {"client": {"clientName": "WEB_EMBEDDED_PLAYER",
|
||||
"clientVersion": "2.20210721.00.00",
|
||||
"clientScreen": "EMBED"}},
|
||||
"header": {"User-Agent": "Mozilla/5.0"},
|
||||
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"},
|
||||
|
||||
"ANDROID_EMBED": {"context": {"client": {"clientName": "ANDROID_EMBEDDED_PLAYER",
|
||||
"clientVersion": "17.31.35",
|
||||
"clientScreen": "EMBED",
|
||||
"androidSdkVersion": 30}},
|
||||
"header": {"User-Agent": "com.google.android.youtube/"},
|
||||
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"}
|
||||
|
||||
# The client is taken from -> https://github.com/JuanBindez/pytubefix
|
||||
"ANDROID": {"context": {"client": {"clientName": "ANDROID",
|
||||
"clientVersion": "19.44.38",
|
||||
"platform": "MOBILE",
|
||||
"osName": "Android",
|
||||
"osVersion": "14",
|
||||
"androidSdkVersion": "34"}},
|
||||
"header": {"User-Agent": "com.google.android.youtube/",
|
||||
"X-Youtube-Client-Name": "3"},
|
||||
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
|
||||
"require_js_player": False,
|
||||
"require_po_token": True}
|
||||
}
|
||||
|
||||
def __init__(self, client="ANDROID_EMBED"):
|
||||
def __init__(self, client="ANDROID"):
|
||||
""" Initialize an InnerTube object.
|
||||
|
||||
@param client: Client to use for the object. Default to web because it returns the most playback types.
|
||||
@@ -339,14 +338,14 @@ class YouTubeDL:
|
||||
return cls._DL_INSTANCE
|
||||
|
||||
def init(self):
|
||||
if not os.path.isfile(f"{self._path}yt_dlp{SEP}version.py"):
|
||||
if os.path.isfile(f"{self._path}yt_dlp{SEP}version.py"):
|
||||
if self._path not in sys.path:
|
||||
sys.path.append(self._path)
|
||||
|
||||
self.init_dl()
|
||||
else:
|
||||
self.get_latest_release()
|
||||
|
||||
if self._path not in sys.path:
|
||||
sys.path.append(self._path)
|
||||
|
||||
self.init_dl()
|
||||
|
||||
def init_dl(self):
|
||||
try:
|
||||
import yt_dlp
|
||||
@@ -361,23 +360,16 @@ class YouTubeDL:
|
||||
log(msg)
|
||||
raise YouTubeException(msg)
|
||||
|
||||
if self._update:
|
||||
if hasattr(yt_dlp.version, "__version__"):
|
||||
l_ver = self.get_last_release_id()
|
||||
cur_ver = yt_dlp.version.__version__
|
||||
if l_ver and yt_dlp.version.__version__ < l_ver:
|
||||
msg = f"yt-dlp has new release!\nCurrent: {cur_ver}. Last: {l_ver}."
|
||||
show_notification(msg)
|
||||
log(msg)
|
||||
self._callback(msg, False)
|
||||
self.get_latest_release()
|
||||
|
||||
self._DownloadError = yt_dlp.utils.DownloadError
|
||||
self._dl = yt_dlp.YoutubeDL(self._OPTIONS)
|
||||
msg = "yt-dlp initialized..."
|
||||
show_notification(msg)
|
||||
log(msg)
|
||||
|
||||
if self._update:
|
||||
if hasattr(yt_dlp.version, "__version__"):
|
||||
self.update(yt_dlp.version.__version__)
|
||||
|
||||
@staticmethod
|
||||
def get_last_release_id():
|
||||
""" Getting last release id. """
|
||||
@@ -388,7 +380,18 @@ class YouTubeDL:
|
||||
except URLError as e:
|
||||
log(f"YouTubeDLHelper error [get last release id]: {e}")
|
||||
|
||||
def get_latest_release(self):
|
||||
@run_task
|
||||
def update(self, current_version):
|
||||
l_ver = self.get_last_release_id()
|
||||
if l_ver and current_version < l_ver:
|
||||
msg = f"yt-dlp has new release!\nCurrent: {current_version}. Last: {l_ver}."
|
||||
show_notification(msg)
|
||||
log(msg)
|
||||
self._callback(msg, False)
|
||||
self.get_latest_release(update=True)
|
||||
|
||||
@run_task
|
||||
def get_latest_release(self, update=False):
|
||||
try:
|
||||
self._is_update_process = True
|
||||
log("Getting the last yt-dlp release...")
|
||||
@@ -426,6 +429,8 @@ class YouTubeDL:
|
||||
raise YouTubeException(e)
|
||||
finally:
|
||||
self._is_update_process = False
|
||||
if not update:
|
||||
self.init()
|
||||
|
||||
def get_yt_link(self, url, skip_errors=False):
|
||||
""" Returns tuple from the video links [dict] and title. """
|
||||
|
||||
@@ -172,6 +172,11 @@
|
||||
<attribute name="label" translatable="yes">Backups</attribute>
|
||||
<attribute name="action">app.on_backup_tool_show</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Boot Logo</attribute>
|
||||
<attribute name="action">app.on_boot_logo_tool_show</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Telnet</attribute>
|
||||
<attribute name="action">app.on_telnet_show</attribute>
|
||||
@@ -393,6 +398,11 @@
|
||||
<attribute name="label" translatable="yes">Backups</attribute>
|
||||
<attribute name="action">app.on_backup_tool_show</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Boot Logo</attribute>
|
||||
<attribute name="action">app.on_boot_logo_tool_show</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Telnet</attribute>
|
||||
<attribute name="action">app.on_telnet_show</attribute>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -41,6 +41,12 @@ from app.ui.dialogs import show_dialog, DialogType, get_builder
|
||||
from app.ui.main_helper import append_text_to_tview, show_info_bar_message
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, HeaderBar
|
||||
|
||||
KEEP_DATA = {"satellites.xml",
|
||||
"terrestrial.xml",
|
||||
"cables.xml",
|
||||
"whitelist",
|
||||
"whitelist_streamrelay"}
|
||||
|
||||
|
||||
class RestoreType(Enum):
|
||||
BOUQUETS = 0
|
||||
@@ -217,11 +223,11 @@ class BackupDialog:
|
||||
self._settings.add("backup_tool_window_size", window.get_size())
|
||||
|
||||
def on_key_release(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
""" Handling keystrokes. """
|
||||
key = KeyboardKey(event.hardware_keycode)
|
||||
if key is KeyboardKey.UNDEFINED:
|
||||
return
|
||||
key = KeyboardKey(key_code)
|
||||
|
||||
ctrl = event.state & MOD_MASK
|
||||
|
||||
if key is KeyboardKey.DELETE:
|
||||
@@ -232,18 +238,19 @@ class BackupDialog:
|
||||
self.restore(RestoreType.BOUQUETS)
|
||||
|
||||
|
||||
def backup_data(path, backup_path, move=True):
|
||||
def backup_data(path, backup_path, move=True, keep=None):
|
||||
""" Creating data backup from a folder at the specified path
|
||||
|
||||
Returns full path to the compressed file.
|
||||
"""
|
||||
keep = keep or KEEP_DATA
|
||||
backup_path = f"{backup_path}{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}{SEP}"
|
||||
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
# Backup files in data dir.
|
||||
for file in filter(lambda f: os.path.isfile(os.path.join(path, f)), os.listdir(path)):
|
||||
src, dst = os.path.join(path, file), backup_path + file
|
||||
shutil.move(src, dst) if move else shutil.copy(src, dst)
|
||||
shutil.move(src, dst) if move and file not in keep else shutil.copy(src, dst)
|
||||
# Compressing to zip and delete remaining files.
|
||||
zip_file = shutil.make_archive(backup_path.rstrip(SEP), "zip", backup_path)
|
||||
shutil.rmtree(backup_path)
|
||||
@@ -259,7 +266,7 @@ def restore_data(src, dst):
|
||||
|
||||
def clear_data_path(path):
|
||||
""" Clearing data at the specified path excluding *.xml file. """
|
||||
for file in filter(lambda f: not f.endswith(".xml") and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
|
||||
for file in filter(lambda f: f not in KEEP_DATA and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
|
||||
os.remove(os.path.join(path, file))
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2024 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
|
||||
@@ -32,14 +32,8 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2024 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkImage" id="details_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">emblem-important-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="main_list_store">
|
||||
<columns>
|
||||
<!-- column-name name -->
|
||||
@@ -48,61 +42,6 @@ Author: Dmitriy Yefremov
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkMenuItem" id="restore_bouquets_popup_menu_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Restore bouquets</property>
|
||||
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="Primary"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuItem" id="restore_all_popup_menu_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Restore all</property>
|
||||
<signal name="activate" handler="on_restore_all" swapped="no"/>
|
||||
<accelerator key="e" signal="activate" modifiers="Primary"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="popup_menu_separator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuItem" id="remove_popup_menu_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Remove</property>
|
||||
<signal name="activate" handler="on_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">user-trash-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="restore_all_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-select-all-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="restore_bouquets_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">document-revert-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkWindow" id="dialog_window">
|
||||
<property name="width-request">560</property>
|
||||
<property name="height-request">320</property>
|
||||
@@ -111,7 +50,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="modal">True</property>
|
||||
<property name="window-position">center-on-parent</property>
|
||||
<property name="destroy-with-parent">True</property>
|
||||
<property name="icon-name">document-revert</property>
|
||||
<property name="icon-name">document-revert-symbolic</property>
|
||||
<signal name="check-resize" handler="on_resize" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
@@ -133,14 +72,21 @@ Author: Dmitriy Yefremov
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="restore_bouquets_header_button">
|
||||
<property name="label" translatable="yes">Restore bouquets</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Restore bouquets</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">restore_bouquets_image</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="on_restore_bouquets" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="restore_bouquets_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">document-revert-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -150,14 +96,21 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="restore_all_header_button">
|
||||
<property name="label" translatable="yes">Restore all</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Restore all</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">restore_all_image</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="on_restore_all" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="restore_all_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-select-all-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -167,14 +120,21 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="remove_header_button">
|
||||
<property name="label" translatable="yes">Remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Remove</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">remove_image</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="on_remove" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="remove_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">user-trash-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="Delete" signal="clicked"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -202,7 +162,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="draw-indicator">False</property>
|
||||
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="details_image1">
|
||||
<object class="GtkImage" id="details_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">emblem-important-symbolic</property>
|
||||
@@ -473,4 +433,41 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkMenuItem" id="restore_bouquets_popup_menu_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Restore bouquets</property>
|
||||
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="Primary"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuItem" id="restore_all_popup_menu_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Restore all</property>
|
||||
<signal name="activate" handler="on_restore_all" swapped="no"/>
|
||||
<accelerator key="e" signal="activate" modifiers="Primary"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="popup_menu_separator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuItem" id="remove_popup_menu_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Remove</property>
|
||||
<signal name="activate" handler="on_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
||||
407
app/ui/bootlogo.py
Normal file
407
app/ui/bootlogo.py
Normal file
@@ -0,0 +1,407 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2026 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
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from ftplib import all_errors
|
||||
from pathlib import Path
|
||||
|
||||
from gi.repository.GObject import BindingFlags
|
||||
|
||||
from app.commons import log, run_task
|
||||
from app.connections import UtfFTP
|
||||
from app.settings import IS_DARWIN, IS_WIN
|
||||
from app.ui.dialogs import translate, get_chooser_dialog, show_dialog, DialogType
|
||||
from app.ui.main_helper import get_picon_pixbuf, redraw_image
|
||||
from app.ui.uicommons import HeaderBar
|
||||
from .uicommons import Gtk, GLib
|
||||
|
||||
_OUTPUT_FILES = ("bootlogo",
|
||||
"bootlogo_wait",
|
||||
"backdrop",
|
||||
"reboot",
|
||||
"shutdown",
|
||||
"radio")
|
||||
_E2_STB_PATHS = ("/usr/share", "/usr/share/enigma2")
|
||||
|
||||
|
||||
class BootLogoManager(Gtk.Window):
|
||||
|
||||
def __init__(self, app, **kwargs):
|
||||
super().__init__(title=translate("Boot Logo"), icon_name="demon-editor", application=app,
|
||||
transient_for=app.app_window, destroy_with_parent=True,
|
||||
window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
|
||||
default_width=560, default_height=320, modal=False, **kwargs)
|
||||
|
||||
self._app = app
|
||||
self._exe = f"{'./' if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS') else ''}ffmpeg"
|
||||
self._pix = None
|
||||
self._img_path = None
|
||||
# subprocess creation flags
|
||||
self._sbp_flags = subprocess.CREATE_NO_WINDOW if IS_WIN else 0
|
||||
|
||||
margin = {"margin_start": 5, "margin_end": 5, "margin_top": 5, "margin_bottom": 5}
|
||||
base_margin = {"margin_start": 10, "margin_end": 10, "margin_top": 10, "margin_bottom": 10}
|
||||
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
frame = Gtk.Frame(shadow_type=Gtk.ShadowType.IN, **base_margin)
|
||||
frame.get_style_context().add_class("view")
|
||||
data_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.VERTICAL, **base_margin)
|
||||
data_box.set_margin_bottom(margin.get("margin_bottom", 5))
|
||||
data_box.set_margin_start(10)
|
||||
frame.add(data_box)
|
||||
self._image_area = Gtk.DrawingArea()
|
||||
self._image_area.connect("draw", self.on_image_draw)
|
||||
data_box.pack_end(self._image_area, True, True, 5)
|
||||
self.add(main_box)
|
||||
# Buttons
|
||||
add_path_button = Gtk.Button.new_from_icon_name("insert-image-symbolic", Gtk.IconSize.BUTTON)
|
||||
add_path_button.set_tooltip_text(translate("Add image"))
|
||||
add_path_button.set_always_show_image(True)
|
||||
add_path_button.connect("clicked", self.on_add_image)
|
||||
receive_button = Gtk.Button.new_from_icon_name("network-receive-symbolic", Gtk.IconSize.BUTTON)
|
||||
receive_button.set_tooltip_text(translate("Download from the receiver"))
|
||||
receive_button.set_always_show_image(True)
|
||||
receive_button.connect("clicked", self.on_receive)
|
||||
transmit_button = Gtk.Button.new_from_icon_name("network-transmit-symbolic", Gtk.IconSize.BUTTON)
|
||||
transmit_button.set_tooltip_text(translate("Transfer to receiver"))
|
||||
transmit_button.set_sensitive(False)
|
||||
transmit_button.set_always_show_image(True)
|
||||
transmit_button.connect("clicked", self.on_transmit)
|
||||
self._convert_button = Gtk.Button.new_from_icon_name("object-rotate-right-symbolic", Gtk.IconSize.BUTTON)
|
||||
self._convert_button.set_tooltip_text(translate("Convert"))
|
||||
self._convert_button.set_always_show_image(True)
|
||||
self._convert_button.set_sensitive(False)
|
||||
self._convert_button.connect("clicked", self.on_convert)
|
||||
self._convert_button.bind_property("sensitive", transmit_button, "sensitive", 4)
|
||||
settings_close_button = Gtk.ModelButton(label=translate("Close"), centered=True, margin_top=5)
|
||||
# Formats.
|
||||
self._format_button = Gtk.ComboBoxText()
|
||||
self._format_button.set_tooltip_text(translate("TV Format"))
|
||||
self._format_button.append("hd720", "HD-Ready (720)")
|
||||
self._format_button.append("hd1080", "Full HD (1080)")
|
||||
self._format_button.set_active_id("hd720")
|
||||
|
||||
action_box = Gtk.ButtonBox()
|
||||
action_box.set_layout(Gtk.ButtonBoxStyle.EXPAND)
|
||||
action_box.add(add_path_button)
|
||||
action_box.add(self._convert_button)
|
||||
action_box.add(self._format_button)
|
||||
data_box.pack_start(action_box, False, False, 0)
|
||||
|
||||
# Settings.
|
||||
self._stb_path_property = "boot_logo_manager_stb_paths"
|
||||
popover = Gtk.Popover()
|
||||
settings_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5, **base_margin)
|
||||
file_name_box = Gtk.Box(spacing=5)
|
||||
file_name_box.add(Gtk.Label(f"{translate('File')}:"))
|
||||
self._file_combo_box = Gtk.ComboBoxText()
|
||||
[self._file_combo_box.append(f"{f}.mvi", f) for f in _OUTPUT_FILES]
|
||||
self._file_combo_box.set_active(0)
|
||||
file_name_box.pack_start(self._file_combo_box, True, True, 0)
|
||||
settings_box.add(file_name_box)
|
||||
|
||||
paths_box = Gtk.Box(spacing=5)
|
||||
paths_box.add(Gtk.Label(translate("STB path:")))
|
||||
self._path_combo_box = Gtk.ComboBoxText(has_entry=True)
|
||||
self._path_entry = self._path_combo_box.get_child()
|
||||
self._path_entry.set_can_focus(False)
|
||||
self._path_entry.connect("focus-out-event", self.on_path_entry_focus_out)
|
||||
# Init paths.
|
||||
self._stb_paths = self._app.app_settings.get(self._stb_path_property, _E2_STB_PATHS)
|
||||
[self._path_combo_box.append(p, p) for p in self._stb_paths]
|
||||
self._path_combo_box.set_active_id(self._stb_paths[0])
|
||||
paths_box.pack_start(self._path_combo_box, True, True, 0)
|
||||
# Paths action box.
|
||||
paths_action_box = Gtk.ButtonBox(homogeneous=True, layout_style=Gtk.ButtonBoxStyle.EXPAND)
|
||||
self._remove_path_button = Gtk.Button.new_from_icon_name("list-remove-symbolic", Gtk.IconSize.BUTTON)
|
||||
self._remove_path_button.set_tooltip_text(translate("Remove"))
|
||||
self._remove_path_button.connect("clicked", self.on_remove_path)
|
||||
add_e2_path_button = Gtk.Button.new_from_icon_name("list-add-symbolic", Gtk.IconSize.BUTTON)
|
||||
add_e2_path_button.set_tooltip_text(translate("Add"))
|
||||
add_e2_path_button.connect("clicked", self.on_add_path)
|
||||
cancel_path_button = Gtk.Button.new_from_icon_name("edit-undo-symbolic", Gtk.IconSize.BUTTON)
|
||||
cancel_path_button.set_tooltip_text(translate("Cancel"))
|
||||
apply_path_button = Gtk.Button.new_from_icon_name("insert-link-symbolic", Gtk.IconSize.BUTTON)
|
||||
apply_path_button.set_tooltip_text(translate("Apply"))
|
||||
apply_path_button.set_can_focus(False)
|
||||
apply_path_button.connect("clicked", self.on_apply_path)
|
||||
|
||||
paths_action_box.add(self._remove_path_button)
|
||||
paths_action_box.add(add_e2_path_button)
|
||||
paths_action_box.add(cancel_path_button)
|
||||
paths_action_box.add(apply_path_button)
|
||||
paths_box.pack_end(paths_action_box, True, True, 0)
|
||||
settings_box.add(paths_box)
|
||||
settings_box.pack_end(settings_close_button, False, False, 0)
|
||||
settings_box.show_all()
|
||||
|
||||
cancel_path_button.set_visible(False)
|
||||
apply_path_button.set_visible(False)
|
||||
self._path_entry.bind_property("has-focus", apply_path_button, "visible")
|
||||
apply_path_button.bind_property("visible", cancel_path_button, "visible")
|
||||
apply_path_button.bind_property("visible", add_e2_path_button, "visible", BindingFlags.INVERT_BOOLEAN)
|
||||
apply_path_button.bind_property("visible", self._remove_path_button, "visible", BindingFlags.INVERT_BOOLEAN)
|
||||
|
||||
popover.add(settings_box)
|
||||
popover.connect("closed", self.on_settings_closed)
|
||||
settings_button = Gtk.MenuButton(popover=popover, valign=Gtk.Align.CENTER, tooltip_text=translate("Options"))
|
||||
settings_button.add(Gtk.Image.new_from_icon_name("applications-system-symbolic", Gtk.IconSize.BUTTON))
|
||||
|
||||
# Header and toolbar.
|
||||
if app.app_settings.use_header_bar:
|
||||
header = HeaderBar(title=translate("Boot Logo"))
|
||||
header.pack_start(receive_button)
|
||||
header.pack_start(transmit_button)
|
||||
header.pack_end(settings_button)
|
||||
|
||||
self.set_titlebar(header)
|
||||
header.show_all()
|
||||
else:
|
||||
toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
toolbar.get_style_context().add_class("primary-toolbar")
|
||||
margin["margin_start"] = 15
|
||||
margin["margin_top"] = 5
|
||||
button_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL, **margin)
|
||||
button_box.pack_start(receive_button, False, False, 0)
|
||||
button_box.pack_start(transmit_button, False, False, 0)
|
||||
toolbar.pack_start(button_box, True, True, 0)
|
||||
toolbar.pack_end(settings_button, False, False, 0)
|
||||
main_box.pack_start(toolbar, False, False, 0)
|
||||
settings_button.set_margin_end(15)
|
||||
|
||||
main_box.pack_start(frame, True, True, 0)
|
||||
main_box.show_all()
|
||||
|
||||
ws_property = "boot_logo_manager_window_size"
|
||||
window_size = self._app.app_settings.get(ws_property, None)
|
||||
if window_size:
|
||||
self.resize(*window_size)
|
||||
|
||||
self.connect("delete-event", lambda w, e: self._app.app_settings.add(ws_property, w.get_size()))
|
||||
self.connect("realize", self.init)
|
||||
|
||||
def init(self, *args):
|
||||
log(f"{self.__class__.__name__} [init] Checking FFmpeg...")
|
||||
try:
|
||||
out = subprocess.check_output([self._exe, "-version"],
|
||||
stderr=subprocess.STDOUT,
|
||||
creationflags=self._sbp_flags)
|
||||
except FileNotFoundError as e:
|
||||
msg = translate("Check if FFmpeg is installed!")
|
||||
self._app.show_error_message(f"Error. {e} {msg}")
|
||||
log(e)
|
||||
else:
|
||||
lines = out.decode(errors="ignore").splitlines()
|
||||
log(lines[0] if lines else lines)
|
||||
|
||||
def on_add_path(self, button):
|
||||
self._path_entry.set_can_focus(True)
|
||||
self._path_entry.grab_focus()
|
||||
|
||||
def on_remove_path(self, button):
|
||||
self._path_combo_box.remove(self._path_combo_box.get_active())
|
||||
self._path_combo_box.set_active(0)
|
||||
self._remove_path_button.set_sensitive(len(self._path_combo_box.get_model()) > 1)
|
||||
|
||||
def on_apply_path(self, button):
|
||||
path = self._path_entry.get_text()
|
||||
paths = {r[0] for r in self._path_combo_box.get_model()}
|
||||
|
||||
if path in paths:
|
||||
self._app.show_error_message("This path already exists!")
|
||||
return True
|
||||
|
||||
self._path_combo_box.append(path, path)
|
||||
self._path_combo_box.set_active_id(path)
|
||||
self._remove_path_button.grab_focus()
|
||||
self._remove_path_button.set_sensitive(len(paths))
|
||||
|
||||
return False
|
||||
|
||||
def on_path_entry_focus_out(self, entry, event):
|
||||
entry.set_can_focus(False)
|
||||
active = self._path_combo_box.get_active_id()
|
||||
txt = entry.get_text()
|
||||
if active != txt:
|
||||
entry.set_text(active or "")
|
||||
|
||||
def on_settings_closed(self, popover):
|
||||
paths = tuple(r[0] for r in self._path_combo_box.get_model())
|
||||
if paths != self._stb_paths:
|
||||
self._stb_paths = paths
|
||||
self._app.app_settings.add(self._stb_path_property, self._stb_paths)
|
||||
|
||||
def on_add_image(self, button):
|
||||
file_filter = None
|
||||
if IS_DARWIN:
|
||||
file_filter = Gtk.FileFilter()
|
||||
file_filter.set_name("*.jpg, *.jpeg, *.png")
|
||||
file_filter.add_mime_type("image/jpeg")
|
||||
file_filter.add_mime_type("image/png")
|
||||
|
||||
response = get_chooser_dialog(self._app.app_window, self._app.app_settings, "*.jpg, *.jpeg, *.png files",
|
||||
("*.jpg", "*.jpeg", "*.png"), "Select image", file_filter)
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
self._img_path = response
|
||||
self._pix = get_picon_pixbuf(response, -1)
|
||||
self._convert_button.set_sensitive(True)
|
||||
self._image_area.queue_draw()
|
||||
|
||||
def on_receive(self, button):
|
||||
self.download_data(self._file_combo_box.get_active_id())
|
||||
|
||||
def on_transmit(self, button):
|
||||
if show_dialog(DialogType.QUESTION, self) != Gtk.ResponseType.OK:
|
||||
return True
|
||||
|
||||
mvi_file = Path(self._img_path).parent.joinpath(self._file_combo_box.get_active_id())
|
||||
if not mvi_file.is_file():
|
||||
log(self._app.show_error_message(translate("No *.mvi file found for the selected image!")))
|
||||
return
|
||||
|
||||
self.transfer_data(mvi_file)
|
||||
|
||||
def on_convert(self, button):
|
||||
self.convert_to_mvi()
|
||||
|
||||
def convert_to_mvi(self, frame_rate=25, bit_rate=2000):
|
||||
path = Path(self._img_path)
|
||||
if not path.is_file():
|
||||
self._app.show_error_message(translate("No image selected!"))
|
||||
return
|
||||
|
||||
output = path.parent.joinpath(self._file_combo_box.get_active_id())
|
||||
if Path(output).exists():
|
||||
msg = f"\n{translate('The file already exists!')}\n\n\t{translate('Are you sure?')}"
|
||||
if show_dialog(DialogType.QUESTION, self, msg) != Gtk.ResponseType.OK:
|
||||
return True
|
||||
|
||||
ffmpeg_output = path.parent.joinpath(f"{self._file_combo_box.get_active_text()}.m2v")
|
||||
|
||||
cmd = [self._exe,
|
||||
"-i", self._img_path,
|
||||
"-r", str(frame_rate),
|
||||
"-b", str(bit_rate),
|
||||
"-s", self._format_button.get_active_id(),
|
||||
ffmpeg_output]
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError as e:
|
||||
self._app.show_error_message(f"{translate('Conversion error.')} {e}")
|
||||
else:
|
||||
with Image.open(self._img_path) as img:
|
||||
width, height = img.size
|
||||
if width != 1280 and height != 720:
|
||||
log(f"{self.__class__.__name__} [convert] Resizing image...")
|
||||
img.resize((1280, 720), Image.Resampling.LANCZOS)
|
||||
tmp = path.parent.joinpath(f"{path.name}.tmp{path.suffix}").absolute()
|
||||
cmd[2] = tmp
|
||||
img.save(tmp)
|
||||
|
||||
# Processing image.
|
||||
log(f"{self.__class__.__name__} [convert] Converting...")
|
||||
subprocess.run(cmd, creationflags=self._sbp_flags)
|
||||
if Path(ffmpeg_output).exists():
|
||||
os.replace(ffmpeg_output, output)
|
||||
log(f"{self.__class__.__name__} [convert] -> '{output}'. Done!")
|
||||
|
||||
if cmd[2] != self._img_path:
|
||||
tmp_path = Path(cmd[2])
|
||||
if tmp_path.exists():
|
||||
tmp_path.unlink()
|
||||
|
||||
self._convert_button.set_sensitive(False)
|
||||
|
||||
def convert_to_image(self, video_path, img_path):
|
||||
cmd = [self._exe, "-y", "-i", video_path, img_path]
|
||||
subprocess.run(cmd, creationflags=self._sbp_flags)
|
||||
|
||||
@run_task
|
||||
def download_data(self, f_name):
|
||||
try:
|
||||
settings = self._app.app_settings
|
||||
with UtfFTP(host=settings.host, port=settings.port, user=settings.user, passwd=settings.password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
ftp.cwd(self._path_combo_box.get_active_id())
|
||||
|
||||
dest = Path(settings.profile_data_path).joinpath("bootlogo")
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
path = f"{dest}{os.sep}"
|
||||
ftp.download_file(f_name, path)
|
||||
vp = Path(f"{path}{f_name}")
|
||||
img_path = f"{path}{f_name}.jpg"
|
||||
|
||||
if vp.exists():
|
||||
rn_path = f"{path}{self._file_combo_box.get_active_text()}.m2v"
|
||||
vp.rename(rn_path)
|
||||
self.convert_to_image(rn_path, img_path)
|
||||
self._pix = get_picon_pixbuf(img_path, -1)
|
||||
GLib.idle_add(self._image_area.queue_draw)
|
||||
|
||||
except all_errors as e:
|
||||
log(f"{self.__class__.__name__} [download error] {e}")
|
||||
GLib.idle_add(self._app.show_error_message, f"{translate('Failed to download data:')} {e}")
|
||||
|
||||
@run_task
|
||||
def transfer_data(self, f_path):
|
||||
try:
|
||||
settings = self._app.app_settings
|
||||
with UtfFTP(host=settings.host, port=settings.port, user=settings.user, passwd=settings.password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
ftp.cwd(self._path_combo_box.get_active_id())
|
||||
|
||||
log(f"{self.__class__.__name__} [transfer data] Creating backup...")
|
||||
backup_path = Path(settings.profile_backup_path).joinpath("bootlogo")
|
||||
backup_path.mkdir(parents=True, exist_ok=True)
|
||||
ftp.download_file(f_path.name, f"{backup_path}{os.sep}")
|
||||
backup_file = backup_path.joinpath(f_path.name)
|
||||
if backup_file.exists():
|
||||
target = backup_path.joinpath(f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}_{f_path.name}")
|
||||
backup_file.rename(target)
|
||||
|
||||
ftp.send_file(f_path.name, f"{f_path.parent}{os.sep}")
|
||||
|
||||
except all_errors as e:
|
||||
log(f"{self.__class__.__name__} [upload error] {e}")
|
||||
GLib.idle_add(self._app.show_error_message, f"{translate('Data transfer error:')} {e}")
|
||||
else:
|
||||
self._app.show_info_message("Done!")
|
||||
|
||||
def on_image_draw(self, area, cr):
|
||||
if self._pix:
|
||||
redraw_image(area, cr, self._pix)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2024 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
|
||||
@@ -32,8 +32,9 @@ import re
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from .main_helper import redraw_image
|
||||
from .dialogs import get_builder, translate
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH
|
||||
from ..commons import run_task, run_with_delay, log, run_idle
|
||||
from ..connections import HttpAPI
|
||||
from ..settings import IS_DARWIN, IS_LINUX, IS_WIN
|
||||
@@ -201,11 +202,7 @@ class ControlTool(Gtk.Box):
|
||||
def on_screenshot_draw(self, area, cr):
|
||||
""" Called to automatically resize the screenshot. """
|
||||
if self._pix:
|
||||
cr.scale(area.get_allocated_width() / self._pix.get_width(),
|
||||
area.get_allocated_height() / self._pix.get_height())
|
||||
img_surface = Gdk.cairo_surface_create_from_pixbuf(self._pix, 1, None)
|
||||
cr.set_source_surface(img_surface, 0, 0)
|
||||
cr.paint()
|
||||
redraw_image(area, cr, self._pix)
|
||||
|
||||
def on_screenshot_all(self, action, value=None):
|
||||
if self._app.http_api:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2026 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
|
||||
@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor. -->
|
||||
<!-- interface-copyright 2018-2024 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2026 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkAboutDialog" id="about_dialog">
|
||||
<property name="can_focus">False</property>
|
||||
@@ -40,8 +40,8 @@ Author: Dmitriy Yefremov
|
||||
<property name="icon_name">system-help</property>
|
||||
<property name="type_hint">normal</property>
|
||||
<property name="program_name">DemonEditor</property>
|
||||
<property name="version">3.9.0 Beta</property>
|
||||
<property name="copyright">2018-2024 Dmitriy Yefremov
|
||||
<property name="version">3.14.4 Beta</property>
|
||||
<property name="copyright">2018-2026 Dmitriy Yefremov
|
||||
</property>
|
||||
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor.</property>
|
||||
<property name="website">https://dyefremov.github.io/DemonEditor/</property>
|
||||
@@ -158,6 +158,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">False</property>
|
||||
<property name="resizable">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="width-request">170</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">splashscreen</property>
|
||||
@@ -166,19 +167,19 @@ Author: Dmitriy Yefremov
|
||||
<property name="decorated">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="wait_dialog_box">
|
||||
<property name="width_request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin_end">10</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="spinner">
|
||||
<property name="width_request">150</property>
|
||||
<property name="height_request">45</property>
|
||||
<property name="visible">True</property>
|
||||
<object class="LoadingProgressBar" id="progress">
|
||||
<property name="visible" bind-source="wait_dialog" bind-property="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="show-text">True</property>
|
||||
<property name="text" translatable="yes">Loading data...</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -186,24 +187,10 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="wait_dialog_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="label" translatable="yes">Loading data...</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child> <!-- NOP -->
|
||||
<style>
|
||||
<class name="app-notification"/>
|
||||
<class name="primary-toolbar"/>
|
||||
</style>
|
||||
</object>
|
||||
</interface>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -34,7 +34,7 @@ from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from app.commons import run_idle
|
||||
from app.settings import SEP, IS_WIN, USE_HEADER_BAR
|
||||
from app.settings import SEP, USE_HEADER_BAR, IS_LINUX
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class BaseDialog(Gtk.Dialog):
|
||||
title=translate(title),
|
||||
modal=True,
|
||||
resizable=False,
|
||||
default_width=240,
|
||||
default_width=255,
|
||||
skip_taskbar_hint=True,
|
||||
skip_pager_hint=True,
|
||||
destroy_with_parent=True,
|
||||
@@ -61,12 +61,12 @@ class Dialog(Enum):
|
||||
MESSAGE = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<requires lib="gtk+" version="3.22"/>
|
||||
<object class="GtkMessageDialog" id="message_dialog">
|
||||
<property name="use-header-bar">{use_header}</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="width_request">250</property>
|
||||
<property name="width_request">255</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
@@ -101,8 +101,8 @@ class WaitDialog:
|
||||
builder, dialog = get_dialog_from_xml(DialogType.WAIT, transient)
|
||||
self._dialog = dialog
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._label = builder.get_object("wait_dialog_label")
|
||||
self._default_text = text or self._label.get_text()
|
||||
self._progress = builder.get_object("progress")
|
||||
self._default_text = text or self._progress.get_text()
|
||||
|
||||
def show(self, text=None):
|
||||
self.set_text(text)
|
||||
@@ -110,7 +110,7 @@ class WaitDialog:
|
||||
|
||||
@run_idle
|
||||
def set_text(self, text):
|
||||
self._label.set_text(translate(text or self._default_text))
|
||||
self._progress.set_text(translate(text or self._default_text))
|
||||
|
||||
@run_idle
|
||||
def hide(self):
|
||||
@@ -228,7 +228,7 @@ def translate(message):
|
||||
|
||||
@lru_cache(maxsize=5)
|
||||
def get_dialogs_string(path, tag="property"):
|
||||
if IS_WIN:
|
||||
if not IS_LINUX:
|
||||
return translate_xml(path, tag)
|
||||
else:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
@@ -257,7 +257,7 @@ def get_builder(path, handlers=None, use_str=False, objects=None, tag="property"
|
||||
|
||||
|
||||
def translate_xml(path, tag="property"):
|
||||
""" Used to translate GUI from * .glade files in MS Windows.
|
||||
""" Used to translate GUI from *.glade files to macOS and MS Windows.
|
||||
|
||||
More info: https://gitlab.gnome.org/GNOME/gtk/-/issues/569
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2
|
||||
<!-- Generated with glade 3.40.0
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -87,9 +87,9 @@ Author: Dmitriy Yefremov
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkMenuItem" id="bouquet_assign_ref_popup_item">
|
||||
<property name="label" translatable="yes">Assign</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Assign</property>
|
||||
<signal name="activate" handler="on_assign_ref" swapped="no"/>
|
||||
<accelerator key="v" signal="activate" modifiers="Primary"/>
|
||||
</object>
|
||||
@@ -217,12 +217,11 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-left">10</property>
|
||||
<property name="margin-right">10</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
@@ -241,7 +240,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="source_selection_box">
|
||||
<property name="visible">True</property>
|
||||
@@ -293,7 +292,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkFileChooserButton" id="xml_chooser_button">
|
||||
<property name="visible">True</property>
|
||||
@@ -399,6 +398,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Filter by presence in the epg.dat file.</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
@@ -432,13 +432,52 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Enables deeper name matching. Possible inaccuracies!</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Enable deep name comparison</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="enable_deep_comparing_switch">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="halign">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_dat_source_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
@@ -551,7 +590,7 @@ Author: Dmitriy Yefremov
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
@@ -565,6 +604,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkSeparator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-top">5</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -576,6 +616,8 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox" id="actions_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<child>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -29,6 +29,7 @@
|
||||
""" Module for working with EPG. """
|
||||
import abc
|
||||
import gzip
|
||||
import json
|
||||
import locale
|
||||
import os
|
||||
import re
|
||||
@@ -43,7 +44,7 @@ from urllib.parse import quote
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_idle, run_task, run_with_delay
|
||||
from app.commons import run_idle, run_task, run_with_delay, log
|
||||
from app.connections import download_data, DownloadType, HttpAPI
|
||||
from app.eparser.ecommons import BouquetService, BqServiceType
|
||||
from app.settings import SEP, EpgSource, IS_WIN
|
||||
@@ -61,7 +62,80 @@ class RefsSource(Enum):
|
||||
XML = 1
|
||||
|
||||
|
||||
class StringComparer:
|
||||
""" Additional string similarity comparer. """
|
||||
|
||||
class ALG(Enum):
|
||||
JARO = "Jaro-Winkler"
|
||||
|
||||
@staticmethod
|
||||
def jaro_distance(s1, s2):
|
||||
""" Returns [Jaro-Winkler] distance of two strings."""
|
||||
if s1 == s2:
|
||||
return 1.0
|
||||
|
||||
len1, len2 = len(s1), len(s2)
|
||||
if len1 == 0 or len2 == 0:
|
||||
return 0.0
|
||||
|
||||
match = 0
|
||||
max_dist = (max(len(s1), len(s2)) // 2) - 1
|
||||
s1_hash = [0] * len(s1)
|
||||
s2_hash = [0] * len(s2)
|
||||
|
||||
for i in range(len1):
|
||||
for j in range(max(0, i - max_dist), min(len2, i + max_dist + 1)):
|
||||
if s1[i] == s2[j] and s2_hash[j] == 0:
|
||||
s1_hash[i] = 1
|
||||
s2_hash[j] = 1
|
||||
match += 1
|
||||
break
|
||||
|
||||
if match == 0:
|
||||
return 0.0
|
||||
|
||||
t = 0
|
||||
point = 0
|
||||
|
||||
for i in range(len1):
|
||||
if s1_hash[i]:
|
||||
while s2_hash[point] == 0:
|
||||
point += 1
|
||||
|
||||
if s1[i] != s2[point]:
|
||||
point += 1
|
||||
t += 1
|
||||
else:
|
||||
point += 1
|
||||
t /= 2
|
||||
|
||||
return (match / len1 + match / len2 + (match - t) / match) / 3.0
|
||||
|
||||
@staticmethod
|
||||
def is_similar(s1, s2, alg, max_ch=4, ratio=0.92):
|
||||
""" Returns similarity of two strings. """
|
||||
if alg is StringComparer.ALG.JARO:
|
||||
dist = StringComparer.jaro_distance(s1, s2)
|
||||
if dist > 0.7:
|
||||
prefix = 0
|
||||
for i in range(min(len(s1), len(s2))):
|
||||
if s1[i] == s2[i]:
|
||||
prefix += 1
|
||||
else:
|
||||
break
|
||||
|
||||
prefix = min(max_ch, prefix) # Maximum of [max_ch] characters are allowed in prefix!
|
||||
dist += 0.1 * prefix * (1 - dist)
|
||||
|
||||
return dist > ratio
|
||||
else:
|
||||
raise ValueError(f"This algorithm [{alg}] is not supported!")
|
||||
|
||||
|
||||
class EpgCache(abc.ABC):
|
||||
_CACHE_FILE = "epg-name-cache"
|
||||
NAME_CACHE = {} # service name -> id (tvg-id for *.m3u)
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__()
|
||||
self.events = {}
|
||||
@@ -83,6 +157,9 @@ class EpgCache(abc.ABC):
|
||||
self._app.connect("epg-settings-changed", self.on_settings_changed)
|
||||
self._app.connect("task-canceled", self.on_xml_load_cancel)
|
||||
|
||||
if self._app.app_settings.enable_epg_name_cache:
|
||||
self.init_name_cache(self._app.app_settings.default_data_path)
|
||||
|
||||
@property
|
||||
def current_reader(self):
|
||||
return self._reader
|
||||
@@ -110,16 +187,20 @@ class EpgCache(abc.ABC):
|
||||
self._canceled = True
|
||||
|
||||
@abc.abstractmethod
|
||||
def reset(self) -> None: pass
|
||||
def reset(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_epg_data(self) -> bool: pass
|
||||
def update_epg_data(self) -> bool:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_current_event(self, service_name) -> EpgEvent: pass
|
||||
def get_current_event(self, service_name) -> EpgEvent:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_current_events(self, service_name) -> list: pass
|
||||
def get_current_events(self, service_name) -> list:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_gz_file_name(url, path):
|
||||
@@ -129,6 +210,30 @@ class EpgCache(abc.ABC):
|
||||
f_sha1 = sha1(url.encode("utf-8", errors="ignore")).hexdigest()
|
||||
return f"{path}epg{os.sep}{f_sha1}_epg.gz"
|
||||
|
||||
@staticmethod
|
||||
@run_task
|
||||
def update_name_cache(path, values):
|
||||
EpgCache.NAME_CACHE.update(values)
|
||||
log(f"[{EpgCache.__name__}] Updating name cache...")
|
||||
f_name = f"{path}{EpgCache._CACHE_FILE}"
|
||||
with open(f_name, "w", encoding="utf-8") as cf:
|
||||
log(f"[{EpgCache.__name__}] Dumping name cache... -> [{f_name}]")
|
||||
json.dump(EpgCache.NAME_CACHE, cf)
|
||||
|
||||
@staticmethod
|
||||
@run_task
|
||||
def init_name_cache(path):
|
||||
f_name = f"{path}{EpgCache._CACHE_FILE}"
|
||||
if not os.path.isfile(f_name):
|
||||
return
|
||||
|
||||
log(f"[{EpgCache.__name__}] Name cache init...")
|
||||
try:
|
||||
with open(f_name, "r", encoding="utf-8") as cf:
|
||||
EpgCache.NAME_CACHE.update(json.load(cf))
|
||||
except Exception as e:
|
||||
log(f"[{EpgCache.__name__}] Name cache init error: {e}")
|
||||
|
||||
|
||||
class FavEpgCache(EpgCache):
|
||||
|
||||
@@ -212,13 +317,18 @@ class FavEpgCache(EpgCache):
|
||||
def update_xml_data(self):
|
||||
services = self._app.current_services
|
||||
names = {services[s].service for s in self._app.current_bouquets.get(self._current_bq, []) if s in services}
|
||||
if self._app.app_settings.enable_epg_name_cache:
|
||||
id_names = set(filter(lambda n: n in EpgCache.NAME_CACHE, names))
|
||||
names -= id_names
|
||||
names.update({EpgCache.NAME_CACHE.get(n) for n in id_names})
|
||||
|
||||
for name, events in self._reader.get_current_events(names).items():
|
||||
ev = min(events, key=lambda x: x.start, default=None)
|
||||
if ev:
|
||||
self.events[name] = ev
|
||||
|
||||
def get_current_event(self, service_name):
|
||||
return self.events.get(service_name, EpgEvent())
|
||||
return self.events.get(EpgCache.NAME_CACHE.get(service_name, service_name), EpgEvent())
|
||||
|
||||
def get_current_events(self, service_name):
|
||||
return [EpgEvent()]
|
||||
@@ -312,6 +422,12 @@ class TabEpgCache(EpgCache):
|
||||
def update_epg_data(self) -> bool:
|
||||
services = self._app.current_services
|
||||
names = {services[s].service for s in chain.from_iterable(self._app.current_bouquets.values()) if s in services}
|
||||
|
||||
if self._app.app_settings.enable_epg_name_cache:
|
||||
id_names = set(filter(lambda n: n in EpgCache.NAME_CACHE, names))
|
||||
names -= id_names
|
||||
names.update({EpgCache.NAME_CACHE.get(n) for n in id_names})
|
||||
|
||||
for name, events in self._reader.get_current_events(names).items():
|
||||
self.events[name] = events
|
||||
|
||||
@@ -323,7 +439,7 @@ class TabEpgCache(EpgCache):
|
||||
pass
|
||||
|
||||
def get_current_events(self, service_name) -> list:
|
||||
return self.events.get(service_name, [])
|
||||
return self.events.get(EpgCache.NAME_CACHE.get(service_name, service_name), [])
|
||||
|
||||
|
||||
class EpgSettingsPopover(Gtk.Popover):
|
||||
@@ -452,6 +568,7 @@ class TabEpgSettingsPopover(EpgSettingsPopover):
|
||||
self._url_combo_box.get_model().clear()
|
||||
[self._url_combo_box.append(i, i) for i in settings.epg_xml_sources if i]
|
||||
self._url_combo_box.set_active_id(settings.epg_xml_source)
|
||||
self._remove_url_button.set_sensitive(len(self._url_combo_box.get_model()) > 1)
|
||||
|
||||
def on_apply(self, button):
|
||||
settings = self._app.app_settings
|
||||
@@ -588,7 +705,11 @@ class EpgTool(Gtk.Box):
|
||||
return
|
||||
|
||||
if self._multi_epg_button.get_active():
|
||||
path = next((r.path for r in self._model if r[-1].get("e2eventservicename", None) == srv.service), None)
|
||||
name = srv.service
|
||||
if self._app.app_settings.enable_epg_name_cache:
|
||||
name = EpgCache.NAME_CACHE.get(name, name)
|
||||
|
||||
path = next((r.path for r in self._model if r[-1].get("e2eventservicename", None) == name), None)
|
||||
scroll_to(path, self._view) if path else None
|
||||
else:
|
||||
self._app.wait_dialog.show()
|
||||
@@ -860,6 +981,8 @@ class EpgDialog:
|
||||
self._left_action_box = builder.get_object("left_action_box")
|
||||
self._xml_download_progress_bar = builder.get_object("xml_download_progress_bar")
|
||||
self._src_load_spinner = builder.get_object("src_load_spinner")
|
||||
self._auto_config_button = builder.get_object("auto_config_button")
|
||||
self._enable_deep_comparing_switch = builder.get_object("enable_deep_comparing_switch")
|
||||
# Filter
|
||||
self._filter_bar = builder.get_object("filter_bar")
|
||||
self._filter_entry = builder.get_object("filter_entry")
|
||||
@@ -1095,10 +1218,10 @@ class EpgDialog:
|
||||
|
||||
def on_key_press(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
key = KeyboardKey(event.hardware_keycode)
|
||||
if key is KeyboardKey.UNDEFINED:
|
||||
return
|
||||
key = KeyboardKey(key_code)
|
||||
|
||||
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
|
||||
|
||||
if ctrl and key is KeyboardKey.C:
|
||||
@@ -1149,12 +1272,17 @@ class EpgDialog:
|
||||
num=r[Column.FAV_NUM])
|
||||
services.append(srv)
|
||||
|
||||
ChannelsParser.write_refs_to_xml("{}{}.xml".format(response, self._bouquet_name), services)
|
||||
ChannelsParser.write_refs_to_xml(f"{response}{self._bouquet_name}.xml", services)
|
||||
self.show_info_message(translate("Done!"), Gtk.MessageType.INFO)
|
||||
|
||||
@run_idle
|
||||
def on_auto_configuration(self, item):
|
||||
gen = self.auto_configuration()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def auto_configuration(self):
|
||||
""" Simple mapping of services by name. """
|
||||
self._auto_config_button.set_sensitive(False)
|
||||
use_cyrillic = locale.getdefaultlocale()[0] in ("ru_RU", "be_BY", "uk_UA", "sr_RS")
|
||||
tr = None
|
||||
if use_cyrillic:
|
||||
@@ -1182,20 +1310,28 @@ class EpgDialog:
|
||||
if ref:
|
||||
self.assign_data(r, ref, True)
|
||||
success_count += 1
|
||||
self._bouquet_epg_count_label.set_text(str(success_count))
|
||||
yield True
|
||||
else:
|
||||
not_founded[name] = r
|
||||
|
||||
# Additional attempt to search in the remaining elements
|
||||
use_deep = self._enable_deep_comparing_switch.get_active()
|
||||
for n in not_founded:
|
||||
for k in source:
|
||||
if k.startswith(n):
|
||||
if StringComparer.is_similar(k, n, StringComparer.ALG.JARO) if use_deep else k.startswith(n):
|
||||
self.assign_data(not_founded[n], source[k], True)
|
||||
success_count += 1
|
||||
self._bouquet_epg_count_label.set_text(str(success_count))
|
||||
break
|
||||
yield True
|
||||
|
||||
self._auto_config_button.set_sensitive(True)
|
||||
self.update_epg_count()
|
||||
self.show_info_message("{} {} {}".format(translate("Done!"),
|
||||
translate("Count of successfully configured services:"),
|
||||
success_count), Gtk.MessageType.INFO)
|
||||
yield True
|
||||
|
||||
def assign_refs(self, model, paths, data):
|
||||
[self.assign_data(model[p], data) for p in paths]
|
||||
@@ -1209,7 +1345,7 @@ class EpgDialog:
|
||||
|
||||
fav_id = row[Column.FAV_ID]
|
||||
fav_id_data = fav_id.split(":")
|
||||
fav_id_data[3:7] = data[-3].split(":")
|
||||
fav_id_data[3:7] = data[-3].split(":")[3:7]
|
||||
|
||||
if data[-2]:
|
||||
row[Column.FAV_POS] = data[-2]
|
||||
@@ -1365,6 +1501,7 @@ class EpgDialog:
|
||||
self._url_to_xml_entry.set_text(epg_options.get("url_to_xml", ""))
|
||||
self._enable_dat_filter = epg_options.get("enable_filtering", False)
|
||||
self._enable_filtering_switch.set_active(self._enable_dat_filter)
|
||||
self._enable_deep_comparing_switch.set_active(epg_options.get("enable_deep_comparing", False))
|
||||
epg_dat_path = epg_options.get("epg_dat_path", epg_dat_path)
|
||||
self._epg_dat_path_entry.set_text(epg_dat_path)
|
||||
self._epg_dat_stb_path_entry.set_text(epg_options.get("epg_dat_stb_path", default_epg_data_stb_path))
|
||||
@@ -1381,6 +1518,7 @@ class EpgDialog:
|
||||
"local_path_to_xml": self._xml_chooser_button.get_filename(),
|
||||
"url_to_xml": self._url_to_xml_entry.get_text(),
|
||||
"enable_filtering": self._enable_filtering_switch.get_active(),
|
||||
"enable_deep_comparing": self._enable_deep_comparing_switch.get_active(),
|
||||
"epg_dat_path": self._epg_dat_path_entry.get_text(),
|
||||
"epg_dat_stb_path": self._epg_dat_stb_path_entry.get_text(),
|
||||
"epg_data_update_on_start": self._update_on_start_switch.get_active()}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2023-2024 Dmitriy Yefremov
|
||||
# Copyright (c) 2023-2026 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
|
||||
@@ -35,7 +35,7 @@ import requests
|
||||
from gi.repository import Gtk, Gdk, GLib, Pango, GObject
|
||||
|
||||
from app.commons import log, run_task, run_idle
|
||||
from app.ui.dialogs import translate
|
||||
from app.ui.dialogs import translate, show_dialog, DialogType
|
||||
from app.ui.uicommons import HeaderBar
|
||||
|
||||
EXT_URL = "https://api.github.com/repos/DYefremov/demoneditor-extensions/contents/extensions/"
|
||||
@@ -48,7 +48,7 @@ HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i686; rv:112.0) Gecko/20100101
|
||||
|
||||
|
||||
class ExtensionManager(Gtk.Window):
|
||||
ICON_INFO = "emblem-important-symbolic"
|
||||
ICON_INFO = "emblem-synchronizing-symbolic"
|
||||
ICON_UPDATE = "network-receive-symbolic"
|
||||
|
||||
class Column(IntEnum):
|
||||
@@ -132,8 +132,8 @@ class ExtensionManager(Gtk.Window):
|
||||
self._load_spinner.bind_property("active", self._view, "sensitive", GObject.BindingFlags.INVERT_BOOLEAN)
|
||||
load_box.pack_end(self._load_spinner, False, False, 0)
|
||||
status_box.pack_end(load_box, False, False, 0)
|
||||
|
||||
data_box.pack_end(status_box, False, True, 0)
|
||||
|
||||
scrolled = Gtk.ScrolledWindow(shadow_type=Gtk.ShadowType.IN)
|
||||
scrolled.add(self._view)
|
||||
data_box.pack_start(scrolled, True, True, 0)
|
||||
@@ -142,27 +142,27 @@ class ExtensionManager(Gtk.Window):
|
||||
self.add(main_box)
|
||||
# Popup menu.
|
||||
menu = Gtk.Menu()
|
||||
item = Gtk.MenuItem.new_with_label(translate("Download"))
|
||||
item.connect("activate", self.on_download)
|
||||
menu.append(item)
|
||||
item = Gtk.MenuItem.new_with_label(translate("Remove"))
|
||||
item.connect("activate", self.on_remove)
|
||||
menu.append(item)
|
||||
download_menu_item = Gtk.MenuItem.new_with_label(translate("Download"))
|
||||
download_menu_item.connect("activate", self.on_download)
|
||||
menu.append(download_menu_item)
|
||||
remove_menu_item = Gtk.MenuItem.new_with_label(translate("Remove"))
|
||||
remove_menu_item.connect("activate", self.on_remove)
|
||||
menu.append(remove_menu_item)
|
||||
menu.show_all()
|
||||
self._view.connect("button-press-event", self.on_view_popup_menu, menu)
|
||||
# Header and toolbar.
|
||||
download_button = Gtk.Button.new_from_icon_name("go-bottom-symbolic", Gtk.IconSize.BUTTON)
|
||||
download_button.set_label(translate("Download"))
|
||||
download_button.set_always_show_image(True)
|
||||
download_button.connect("clicked", self.on_download)
|
||||
self._download_button = Gtk.Button.new_from_icon_name("go-bottom-symbolic", Gtk.IconSize.BUTTON)
|
||||
self._download_button.set_tooltip_text(translate("Download"))
|
||||
self._download_button.set_always_show_image(True)
|
||||
self._download_button.connect("clicked", self.on_download)
|
||||
remove_button = Gtk.Button.new_from_icon_name("user-trash-symbolic", Gtk.IconSize.BUTTON)
|
||||
remove_button.set_label(translate("Remove"))
|
||||
remove_button.set_tooltip_text(translate("Remove"))
|
||||
remove_button.set_always_show_image(True)
|
||||
remove_button.connect("clicked", self.on_remove)
|
||||
|
||||
if app.app_settings.use_header_bar:
|
||||
header = HeaderBar()
|
||||
header.pack_start(download_button)
|
||||
header.pack_start(self._download_button)
|
||||
header.pack_start(remove_button)
|
||||
|
||||
self.set_titlebar(header)
|
||||
@@ -170,14 +170,21 @@ class ExtensionManager(Gtk.Window):
|
||||
else:
|
||||
toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
toolbar.get_style_context().add_class("primary-toolbar")
|
||||
button_box = Gtk.Box(spacing=2, orientation=Gtk.Orientation.HORIZONTAL, **margin)
|
||||
button_box.pack_start(download_button, False, False, 0)
|
||||
margin["margin_start"] = 15
|
||||
margin["margin_top"] = 10
|
||||
button_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL, **margin)
|
||||
button_box.pack_start(self._download_button, False, False, 0)
|
||||
button_box.pack_start(remove_button, False, False, 0)
|
||||
toolbar.pack_start(button_box, True, True, 0)
|
||||
main_box.pack_start(toolbar, False, False, 0)
|
||||
|
||||
main_box.pack_start(frame, True, True, 0)
|
||||
main_box.show_all()
|
||||
# Connection status.
|
||||
self._connection_status_image = Gtk.Image.new_from_icon_name("network-offline-symbolic", Gtk.IconSize.BUTTON)
|
||||
status_box.pack_end(self._connection_status_image, False, False, 0)
|
||||
self._download_button.bind_property("visible", self._connection_status_image, "visible", 4)
|
||||
self._download_button.bind_property("visible", download_menu_item, "visible")
|
||||
|
||||
ws_property = "extension_manager_window_size"
|
||||
window_size = self._app.app_settings.get(ws_property, None)
|
||||
@@ -186,6 +193,22 @@ class ExtensionManager(Gtk.Window):
|
||||
|
||||
self.connect("delete-event", lambda w, e: self._app.app_settings.add(ws_property, w.get_size()))
|
||||
self.connect("realize", self.init)
|
||||
self.connect("show", self.on_show)
|
||||
|
||||
def on_show(self, window):
|
||||
enabled = self._app.app_settings.extensions_support
|
||||
self.set_sensitive(enabled)
|
||||
if not enabled:
|
||||
msg = f"\n{translate('Extension support is disabled!')}\n\n\t{translate('Do you want to enable it?')}"
|
||||
if show_dialog(DialogType.QUESTION, self, msg) != Gtk.ResponseType.OK:
|
||||
self.close()
|
||||
return True
|
||||
|
||||
self._app.app_settings.extensions_support = True
|
||||
self._app.show_info_message(translate('Restart the program to apply all changes.'), Gtk.MessageType.WARNING)
|
||||
self.close()
|
||||
|
||||
return False
|
||||
|
||||
def init(self, widget):
|
||||
self._load_spinner.start()
|
||||
@@ -225,20 +248,23 @@ class ExtensionManager(Gtk.Window):
|
||||
|
||||
@run_task
|
||||
def update(self):
|
||||
with requests.get(url=EXT_LIST_FILE, stream=True) as resp:
|
||||
error_msg = None
|
||||
if resp.status_code == 200:
|
||||
try:
|
||||
self.update_data(resp.json())
|
||||
except ValueError as e:
|
||||
error_msg = f"{self.__class__.__name__} [update] error: {e}"
|
||||
else:
|
||||
error_msg = f"{self.__class__.__name__} [update] error: {resp.reason}"
|
||||
error_msg = None
|
||||
try:
|
||||
with requests.get(url=EXT_LIST_FILE, stream=True) as resp:
|
||||
if resp.status_code == 200:
|
||||
try:
|
||||
self.update_data(resp.json())
|
||||
except ValueError as e:
|
||||
error_msg = f"{self.__class__.__name__} [update] error: {e}"
|
||||
else:
|
||||
error_msg = f"{self.__class__.__name__} [update] error: {resp.reason}"
|
||||
GLib.idle_add(self._app.show_error_message, "Data loading error!")
|
||||
except OSError as e:
|
||||
error_msg = f"{self.__class__.__name__} [update] error: Connection error. {e}"
|
||||
|
||||
if error_msg:
|
||||
log(error_msg)
|
||||
GLib.idle_add(self._load_spinner.stop)
|
||||
GLib.idle_add(self._app.show_error_message, "Data loading error!")
|
||||
if error_msg:
|
||||
log(error_msg)
|
||||
self.update_local_data()
|
||||
|
||||
@run_idle
|
||||
def update_data(self, data):
|
||||
@@ -246,6 +272,16 @@ class ExtensionManager(Gtk.Window):
|
||||
gen = self.append_data(data)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
@run_idle
|
||||
def update_local_data(self):
|
||||
self._download_button.set_visible(False)
|
||||
self._load_spinner.stop()
|
||||
self._model.clear()
|
||||
|
||||
for ext, d in self.get_installed().items():
|
||||
e, path = d
|
||||
self._model.append((e.LABEL, None, e.VERSION, None, path, ext, None, path))
|
||||
|
||||
def append_data(self, data):
|
||||
installed = self.get_installed()
|
||||
for e, d in data.items():
|
||||
@@ -261,6 +297,7 @@ class ExtensionManager(Gtk.Window):
|
||||
ext_ver = ext[0].VERSION
|
||||
path = ext[1]
|
||||
if ext_ver < ver:
|
||||
desc = f"[ Update -> ver. {ver} ] {desc}"
|
||||
ver = ext_ver
|
||||
info = self.ICON_INFO
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -45,7 +45,7 @@ from app.connections import UtfFTP
|
||||
from app.settings import IS_LINUX, IS_DARWIN, IS_WIN, SEP, USE_HEADER_BAR
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_builder, translate
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, Page
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, Page, LINK_ICON, FOLDER_ICON
|
||||
|
||||
File = namedtuple("File", ["icon", "name", "size", "date", "attr", "extra"])
|
||||
|
||||
@@ -296,12 +296,6 @@ class FtpClientBox(Gtk.HBox):
|
||||
# Force Ctrl
|
||||
self._ftp_view.connect("key-press-event", self._app.force_ctrl)
|
||||
self._file_view.connect("key-press-event", self._app.force_ctrl)
|
||||
# Icons
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
folder_icon = "folder-symbolic" if settings.is_darwin else "folder"
|
||||
self._folder_icon = theme.load_icon(folder_icon, 16, 0) if theme.lookup_icon(folder_icon, 16, 0) else None
|
||||
self._link_icon = theme.load_icon("emblem-symbolic-link", 16, 0) if theme.lookup_icon("emblem-symbolic-link",
|
||||
16, 0) else None
|
||||
# Initialization
|
||||
self.init_drag_and_drop()
|
||||
self.init_ftp()
|
||||
@@ -324,7 +318,8 @@ class FtpClientBox(Gtk.HBox):
|
||||
if self._ftp:
|
||||
self._ftp.close()
|
||||
|
||||
self._ftp = UtfFTP(host=self._settings.host, user=self._settings.user, passwd=self._settings.password)
|
||||
host, port = self._settings.host, self._settings.port
|
||||
self._ftp = UtfFTP(host=host, port=port, user=self._settings.user, passwd=self._settings.password)
|
||||
self._ftp.encoding = "utf-8"
|
||||
self.update_ftp_info(self._ftp.getwelcome())
|
||||
except all_errors as e:
|
||||
@@ -377,10 +372,10 @@ class FtpClientBox(Gtk.HBox):
|
||||
icon = None
|
||||
if is_dir:
|
||||
r_size = self.FOLDER
|
||||
icon = self._folder_icon
|
||||
icon = FOLDER_ICON
|
||||
elif p.is_symlink():
|
||||
r_size = self.LINK
|
||||
icon = self._link_icon
|
||||
icon = LINK_ICON
|
||||
else:
|
||||
r_size = get_size_from_bytes(size)
|
||||
|
||||
@@ -401,10 +396,10 @@ class FtpClientBox(Gtk.HBox):
|
||||
icon = None
|
||||
if is_dir:
|
||||
r_size = self.FOLDER
|
||||
icon = self._folder_icon
|
||||
icon = FOLDER_ICON
|
||||
elif is_link:
|
||||
r_size = self.LINK
|
||||
icon = self._link_icon
|
||||
icon = LINK_ICON
|
||||
else:
|
||||
r_size = get_size_from_bytes(size)
|
||||
|
||||
@@ -675,7 +670,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
log(e)
|
||||
self._app.show_error_message(str(e))
|
||||
else:
|
||||
itr = self._file_model.append(File(self._folder_icon, path.name, self.FOLDER, "", str(path.resolve()), "0"))
|
||||
itr = self._file_model.append(File(FOLDER_ICON, path.name, self.FOLDER, "", str(path.resolve()), "0"))
|
||||
renderer.set_property("editable", True)
|
||||
self._file_view.set_cursor(self._file_model.get_path(itr), self._file_view.get_column(0), True)
|
||||
|
||||
@@ -695,7 +690,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
log(e)
|
||||
else:
|
||||
if resp == f"{cur_path}/{name}":
|
||||
itr = self._ftp_model.append(File(self._folder_icon, name, self.FOLDER, "", "drwxr-xr-x", "0"))
|
||||
itr = self._ftp_model.append(File(FOLDER_ICON, name, self.FOLDER, "", "drwxr-xr-x", "0"))
|
||||
renderer.set_property("editable", True)
|
||||
self._ftp_view.set_cursor(self._ftp_model.get_path(itr), self._ftp_view.get_column(0), True)
|
||||
|
||||
@@ -861,11 +856,10 @@ class FtpClientBox(Gtk.HBox):
|
||||
self._settings.ftp_bookmarks = [r[0] for r in self._bookmark_model]
|
||||
|
||||
def on_view_key_press(self, view, event):
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
key = KeyboardKey(event.hardware_keycode)
|
||||
if key is KeyboardKey.UNDEFINED:
|
||||
return
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & MOD_MASK
|
||||
|
||||
if key is KeyboardKey.F7:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2025 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
|
||||
@@ -97,7 +97,7 @@ def import_bouquet(app, model, path, appender, file_path=None):
|
||||
|
||||
def get_enigma2_bouquet(path):
|
||||
p = Path(path)
|
||||
bq = BouquetsReader.get_bouquet(f"{p.parent}{SEP}", f"{p.stem}{p.suffix}", p.stem)
|
||||
bq = BouquetsReader().get_bouquet(f"{p.parent}{SEP}", f"{p.stem}{p.suffix}", p.stem)
|
||||
bouquet = Bouquet(name=bq[0], type=BqType(p.suffix.lstrip(".")).value, services=bq[1], locked=None, hidden=None)
|
||||
return bouquet
|
||||
|
||||
@@ -194,7 +194,11 @@ class ImportDialog:
|
||||
try:
|
||||
if not self._bouquets:
|
||||
log("Import [init data]: getting bouquets...")
|
||||
self._bouquets = get_bouquets(path, self._profile)
|
||||
self._bouquets, errors = get_bouquets(path, self._profile)
|
||||
if errors:
|
||||
msg = translate('There were errors [%s] during bouquets loading!') % errors
|
||||
self.show_info_message(f"{msg} {translate('Check the log for more info.')}",
|
||||
Gtk.MessageType.WARNING)
|
||||
for bqs in self._bouquets:
|
||||
for bq in bqs.bouquets:
|
||||
self._bq_model.append((bq.name, bq.type, True))
|
||||
@@ -422,10 +426,9 @@ class ImportDialog:
|
||||
|
||||
def on_key_press(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
key = KeyboardKey(event.hardware_keycode)
|
||||
if key is KeyboardKey.UNDEFINED:
|
||||
return
|
||||
key = KeyboardKey(key_code)
|
||||
|
||||
if key is KeyboardKey.SPACE:
|
||||
model = view.get_model()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2024 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
|
||||
@@ -32,7 +32,7 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2024 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkImage" id="remove_selection_image">
|
||||
<property name="visible">True</property>
|
||||
@@ -270,13 +270,14 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
<object class="GtkDialog" id="iptv_list_configuration_dialog">
|
||||
<property name="use-header-bar">{use_header}</property>
|
||||
<property name="width-request">400</property>
|
||||
<property name="width-request">680</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="title" translatable="yes">IPTV streams list configuration</property>
|
||||
<property name="resizable">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window-position">center</property>
|
||||
<property name="destroy-with-parent">True</property>
|
||||
<property name="icon-name">demon-editor</property>
|
||||
<property name="type-hint">dialog</property>
|
||||
<property name="skip-taskbar-hint">True</property>
|
||||
<property name="skip-pager-hint">True</property>
|
||||
@@ -512,9 +513,9 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="list_namespace_entry">
|
||||
<property name="width-request">120</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="width-chars">5</property>
|
||||
<property name="max-width-chars">5</property>
|
||||
@@ -879,6 +880,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="resizable">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="destroy-with-parent">True</property>
|
||||
<property name="icon-name">demon-editor</property>
|
||||
<property name="type-hint">dialog</property>
|
||||
<property name="skip-taskbar-hint">True</property>
|
||||
<property name="skip-pager-hint">True</property>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -46,6 +46,7 @@ from app.eparser.iptv import (NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID
|
||||
from app.settings import SettingsType
|
||||
from app.tools.yt import YouTubeException, YouTube
|
||||
from app.ui.dialogs import Action, show_dialog, DialogType, translate, get_builder, BaseDialog
|
||||
from app.ui.epg.epg import EpgCache
|
||||
from app.ui.main_helper import get_iptv_url, on_popup_menu, get_picon_pixbuf, show_info_bar_message, gen_bouquet_name
|
||||
from app.ui.uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, IPTV_ICON, Column, KeyboardKey, get_yt_icon, HeaderBar)
|
||||
|
||||
@@ -769,6 +770,8 @@ class M3uImportDialog(IptvListDialog):
|
||||
|
||||
picons = {}
|
||||
services = self._services
|
||||
if self._app.app_settings.enable_epg_name_cache:
|
||||
EpgCache.update_name_cache(self._app.app_settings.default_data_path, {s[3]: s[0] for s in services if s[0]})
|
||||
|
||||
if not self.is_all_data_default():
|
||||
services = []
|
||||
@@ -1328,10 +1331,9 @@ class YtListImportDialog:
|
||||
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 2, select))
|
||||
|
||||
def on_key_press(self, view, event):
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
key = KeyboardKey(event.hardware_keycode)
|
||||
if key is KeyboardKey.UNDEFINED:
|
||||
return
|
||||
key = KeyboardKey(key_code)
|
||||
|
||||
if key is KeyboardKey.SPACE:
|
||||
path, column = view.get_cursor()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/ui/lang/sk/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/sk/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2025 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
|
||||
@@ -32,7 +32,7 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor. -->
|
||||
<!-- interface-copyright 2018-2024 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2025 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkListStore" id="alt_list_store">
|
||||
<columns>
|
||||
@@ -57,12 +57,23 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkMenu" id="alt_popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkMenuItem" id="alt_paste_popup_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Paste</property>
|
||||
<signal name="activate" handler="on_alt_paste" swapped="no"/>
|
||||
<accelerator key="v" signal="activate" modifiers="Primary"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuItem" id="alt_remove_popup_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Remove</property>
|
||||
<signal name="activate" handler="on_delete" object="alt_tree_view" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@@ -298,6 +309,67 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkPopover" id="filter_cas_popover">
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox">
|
||||
<property name="width-request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="filter_all_button">
|
||||
<property name="label" translatable="yes">All</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<signal name="toggled" handler="on_filter_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="filter_free_button">
|
||||
<property name="label" translatable="yes">Free (FTA)</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">filter_coded_button</property>
|
||||
<signal name="toggled" handler="on_filter_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="filter_coded_button">
|
||||
<property name="label" translatable="yes">Coded</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">filter_all_button</property>
|
||||
<signal name="toggled" handler="on_filter_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkListStore" id="filter_sat_pos_list_store">
|
||||
<columns>
|
||||
<!-- column-name satellite -->
|
||||
@@ -1130,7 +1202,7 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator">
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_4">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
@@ -1145,7 +1217,31 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_4">
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_5">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuItem" id="fav_use_sr_popup_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Use with Streamrelay</property>
|
||||
<signal name="activate" handler="on_use_streamrelay" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuItem" id="fav_remove_sr_popup_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Remove use with Streamrelay</property>
|
||||
<signal name="activate" handler="on_remove_use_streamrelay" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_6">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
@@ -1172,7 +1268,7 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_5">
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_7">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
@@ -1194,7 +1290,7 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_6">
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_8">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
@@ -1217,7 +1313,7 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_7">
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_9">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
@@ -1231,7 +1327,7 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_8">
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_10">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
@@ -1249,7 +1345,7 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_9">
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_11">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
@@ -1292,7 +1388,7 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="fav_pupup_separator_10">
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_12">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
@@ -1310,7 +1406,7 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_11">
|
||||
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_13">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
@@ -1455,9 +1551,42 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuItem" id="services_add_new_popup_item">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">New</property>
|
||||
<signal name="activate" handler="on_services_add_new" swapped="no"/>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child type="submenu">
|
||||
<object class="GtkMenu" id="et_popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkMenuItem">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Satellite channel</property>
|
||||
<property name="name">s</property>
|
||||
<signal name="activate" handler="on_services_add_new" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuItem">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Terrestrial channel</property>
|
||||
<property name="name">t</property>
|
||||
<signal name="activate" handler="on_services_add_new" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuItem">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Cable channel</property>
|
||||
<property name="name">c</property>
|
||||
<signal name="activate" handler="on_services_add_new" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@@ -1572,6 +1701,7 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
<object class="GtkComboBoxText" id="profile_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive" bind-source="iptv_progress_bar" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="active">0</property>
|
||||
@@ -1722,7 +1852,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkLabel" id="app_ver_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label">3.9.0 Beta</property>
|
||||
<property name="label">3.14.4 Beta</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
@@ -1776,6 +1906,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="label" translatable="yes">DVB</property>
|
||||
<property name="width-request">80</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive" bind-source="iptv_progress_bar" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
@@ -1792,6 +1923,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkRadioButton" id="iptv_button">
|
||||
<property name="label" translatable="yes">IPTV</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive" bind-source="services_progress_bar" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
@@ -1815,7 +1947,7 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="filter_services_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="sensitive" bind-source="services_progress_bar" bind-property="visible" bind-flags="invert-boolean">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
@@ -1837,6 +1969,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="filter_iptv_services_button">
|
||||
<property name="sensitive" bind-source="iptv_progress_bar" bind-property="visible" bind-flags="invert-boolean">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
@@ -1998,22 +2131,23 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox" id="services_fs_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-left">5</property>
|
||||
<property name="margin-right">5</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="spacing">10</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="filter_box">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="filter_only_free_button">
|
||||
<object class="GtkMenuButton" id="filter_cas_menu_button">
|
||||
<property name="width-request">50</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Only free</property>
|
||||
<signal name="toggled" handler="on_filter_changed" swapped="no"/>
|
||||
<property name="tooltip-text" translatable="yes">Access</property>
|
||||
<property name="popover">filter_cas_popover</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
@@ -2595,8 +2729,8 @@ Author: Dmitriy Yefremov
|
||||
<property name="height-request">26</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-left">5</property>
|
||||
<property name="margin-right">5</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="resize-mode">queue</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
@@ -2729,12 +2863,13 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="services_load_spinner">
|
||||
<property name="visible">True</property>
|
||||
<object class="GtkProgressBar" id="services_progress_bar">
|
||||
<property name="visible">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Loading data...</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin-start">15</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -2743,6 +2878,37 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">10</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImage" id="current_data_path_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Current data path</property>
|
||||
<property name="icon-name">document-open-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">11</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="current_data_path_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Current data path</property>
|
||||
<property name="label"> </property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="xalign">0</property>
|
||||
<attributes>
|
||||
<attribute name="size" value="8000"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">12</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -3077,8 +3243,8 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="iptv_services_load_spinner">
|
||||
<property name="visible">True</property>
|
||||
<object class="GtkProgressBar" id="iptv_progress_bar">
|
||||
<property name="visible">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Loading data...</property>
|
||||
<property name="halign">center</property>
|
||||
@@ -3706,6 +3872,8 @@ Author: Dmitriy Yefremov
|
||||
<signal name="drag-begin" handler="on_view_drag_begin" after="yes" swapped="no"/>
|
||||
<signal name="drag-data-get" handler="on_view_drag_data_get" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_alt_view_drag_data_received" swapped="no"/>
|
||||
<signal name="focus-in-event" handler="on_view_focus" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_alt_view_key_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_alt_selection" object="alt_list_store" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection"/>
|
||||
@@ -4638,97 +4806,185 @@ Author: Dmitriy Yefremov
|
||||
<property name="can-focus">False</property>
|
||||
<property name="stack">stack</property>
|
||||
</object>
|
||||
<object class="GtkButtonBox" id="toolbar_main_box">
|
||||
<object class="GtkPopover" id="send_popover">
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox">
|
||||
<property name="width-request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkModelButton" id="send_srv_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="action-name">app.on_send</property>
|
||||
<property name="text" translatable="yes">All</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkModelButton" id="send_bq_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="action-name">app.upload_bouquets</property>
|
||||
<property name="text" translatable="yes">Bouquets</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkBox" id="toolbar_main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="open_tool_button">
|
||||
<object class="GtkButtonBox" id="osv_button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Open</property>
|
||||
<property name="action-name">app.on_data_open</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="open_image">
|
||||
<object class="GtkButton" id="open_tool_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">document-open-symbolic</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Open</property>
|
||||
<property name="action-name">app.on_data_open</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="open_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">document-open-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="save_tool_button">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Save</property>
|
||||
<property name="action-name">app.on_data_save</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="save_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">document-save-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="receive_button">
|
||||
<object class="GtkButtonBox" id="send_receive_button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Download from the receiver</property>
|
||||
<property name="action-name">app.on_receive</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="download_image">
|
||||
<property name="width-request">32</property>
|
||||
<object class="GtkButton" id="receive_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">network-receive-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Download from the receiver</property>
|
||||
<property name="action-name">app.on_receive</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="download_image">
|
||||
<property name="width-request">32</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">network-receive-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="send_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Transfer to receiver</property>
|
||||
<property name="action-name">app.on_send</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="send_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">network-transmit-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="send_menu_button">
|
||||
<property name="visible" bind-source="send_button" bind-property="visible" bind-flags="invert-boolean">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Transfer to receiver</property>
|
||||
<property name="popover">send_popover</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="send_menu_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">network-transmit-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="send_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Transfer to receiver</property>
|
||||
<property name="action-name">app.on_send</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="send_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">network-transmit-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="save_tool_button">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Save</property>
|
||||
<property name="action-name">app.on_data_save</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="save_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">document-save-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
502
app/ui/main.py
502
app/ui/main.py
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,7 @@ from gi.repository import GdkPixbuf, GLib, Gio
|
||||
|
||||
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.eparser.enigma.bouquets import BqServiceType
|
||||
from app.settings import SettingsType, SEP, IS_WIN, IS_DARWIN, IS_LINUX
|
||||
from .dialogs import show_dialog, DialogType, translate
|
||||
from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column
|
||||
@@ -294,7 +294,7 @@ def set_lock(blacklist, services, model, paths, target, services_model):
|
||||
fav_id = model.get_value(itr, Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID)
|
||||
srv = services.get(fav_id, None)
|
||||
if srv and srv.service_type not in skip_type:
|
||||
bq_id = srv.data_id if srv.service_type == BqServiceType.IPTV.name else to_bouquet_id(srv)
|
||||
bq_id = srv.data_id if srv.service_type == BqServiceType.IPTV.name else srv.fav_id
|
||||
if not bq_id:
|
||||
continue
|
||||
blacklist.discard(bq_id) if locked else blacklist.add(bq_id)
|
||||
@@ -819,7 +819,10 @@ def get_pos_num(pos):
|
||||
|
||||
if len(pos) > 1:
|
||||
m = -1 if pos[-1] == "W" else 1
|
||||
return float(pos[:-1]) * m
|
||||
try:
|
||||
return float(pos[:-1]) * m
|
||||
except ValueError:
|
||||
return -183
|
||||
|
||||
return -181.0 if pos == "T" else -182.0
|
||||
|
||||
@@ -855,7 +858,7 @@ def get_iptv_data(fav_id):
|
||||
|
||||
|
||||
def on_popup_menu(menu, event):
|
||||
""" Shows popup menu for the view """
|
||||
""" Shows popup menu for the view. """
|
||||
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)
|
||||
|
||||
@@ -868,5 +871,14 @@ def show_info_bar_message(bar, label, text, message_type=Gtk.MessageType.INFO):
|
||||
bar.set_visible(True)
|
||||
|
||||
|
||||
def redraw_image(area, cr, pixbuf):
|
||||
""" Helper method to redraw (auto resize) image in the Gtk DrawingArea. """
|
||||
cr.scale(area.get_allocated_width() / pixbuf.get_width(),
|
||||
area.get_allocated_height() / pixbuf.get_height())
|
||||
img_surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, 1, None)
|
||||
cr.set_source_surface(img_surface, 0, 0)
|
||||
cr.paint()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -347,7 +347,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="info_check_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="visible" bind-source="filter_button" bind-property="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Details</property>
|
||||
@@ -413,12 +413,13 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="header_download_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="visible" bind-source="cancel_button" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
<property name="visible" bind-source="convert_button" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="add_menu_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="visible" bind-source="manager_button" bind-property="active">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
@@ -440,6 +441,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="receive_button">
|
||||
<property name="visible" bind-source="download_source_button" bind-property="visible">False</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">False</property>
|
||||
@@ -462,7 +464,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="remove_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="visible" bind-source="filter_button" bind-property="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Remove all picons from the receiver</property>
|
||||
@@ -490,7 +492,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="src_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="visible" bind-source="filter_button" bind-property="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Additional source</property>
|
||||
@@ -529,6 +531,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="filter_bar">
|
||||
<property name="visible" bind-source="filter_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="spacing">5</property>
|
||||
@@ -591,6 +594,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="wide-handle">True</property>
|
||||
<child>
|
||||
<object class="GtkFrame" id="src_picon_box_frame">
|
||||
<property name="visible" bind-source="src_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label-xalign">0.49000000953674316</property>
|
||||
<property name="shadow-type">none</property>
|
||||
@@ -607,6 +611,7 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="src_filter_button">
|
||||
<property name="label" translatable="yes">Filter</property>
|
||||
<property name="visible" bind-source="filter_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">False</property>
|
||||
@@ -741,6 +746,7 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="dst_filter_button">
|
||||
<property name="label" translatable="yes">Filter</property>
|
||||
<property name="visible" bind-source="filter_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">False</property>
|
||||
@@ -849,6 +855,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="explorer_dst_label">
|
||||
<property name="visible" bind-source="src_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Destination:</property>
|
||||
</object>
|
||||
@@ -868,6 +875,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="explorer_info_box_frame">
|
||||
<property name="visible" bind-source="info_check_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label-xalign">0</property>
|
||||
<property name="shadow-type">in</property>
|
||||
@@ -952,7 +960,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="margin-bottom">5</property>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="download_source_button">
|
||||
<property name="sensitive">False</property>
|
||||
<property name="sensitive" bind-source="satellite_label" bind-property="visible">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Source:</property>
|
||||
<property name="active">0</property>
|
||||
@@ -1026,7 +1034,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="loading_data_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="visible" bind-source="satellite_label" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Loading data...</property>
|
||||
<property name="ellipsize">end</property>
|
||||
@@ -1039,10 +1047,10 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="loading_data_spinner">
|
||||
<property name="visible">True</property>
|
||||
<property name="visible" bind-source="satellite_label" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-right">5</property>
|
||||
<property name="active">True</property>
|
||||
<property name="active" bind-source="satellite_label" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -1055,6 +1063,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkGrid" id="satellite_filter_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Filter by current satellite positions</property>
|
||||
<property name="margin-right">5</property>
|
||||
<property name="column-spacing">5</property>
|
||||
<child>
|
||||
@@ -1072,7 +1081,6 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkSwitch" id="satellite_filter_switch">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="active">True</property>
|
||||
<signal name="state-set" handler="on_satellite_filter_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1103,6 +1111,7 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkTreeView" id="satellites_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive" bind-source="satellite_label" bind-property="visible">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="model">satellites_list_store</property>
|
||||
<property name="headers-visible">False</property>
|
||||
@@ -1571,110 +1580,24 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox" id="converter_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-left">5</property>
|
||||
<property name="margin-right">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<!-- n-columns=3 n-rows=4 -->
|
||||
<object class="GtkGrid" id="converter_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-left">10</property>
|
||||
<property name="margin-right">10</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="row-spacing">5</property>
|
||||
<property name="column-spacing">2</property>
|
||||
<property name="column-homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkFileChooserButton" id="enigma2_path_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="action">select-folder</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="picons_path_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Path to Enigma2 picons:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="save_to_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Path to save:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFileChooserButton" id="save_to_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="action">select-folder</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="convert_to_label">
|
||||
<property name="visible">True</property>
|
||||
<object class="GtkLabel" id="convert_to_nt_label">
|
||||
<property name="visible" bind-source="converter_nt_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="label" translatable="yes">Enigma2 -> Neutrino-MP</property>
|
||||
<property name="label">Enigma2 -> Neutrino-MP</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
@@ -1688,9 +1611,154 @@ Author: Dmitriy Yefremov
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="convert_to_sc_label">
|
||||
<property name="visible" bind-source="converter_sc_button" bind-property="active">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="label">Enigma2 -> OSCam</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="converter_format_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="converter_sc_button">
|
||||
<property name="label" translatable="yes">OSCam</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">converter_nt_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="converter_nt_button">
|
||||
<property name="label" translatable="yes">Neutrino-MP</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">converter_sc_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="converter_select_bq_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="coverter_bq_label">
|
||||
<property name="label" translatable="yes">Convert for selected bouquets</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="converter_bq_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="picons_path_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Path to Enigma2 picons:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFileChooserButton" id="enigma2_path_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="action">select-folder</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="save_to_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Path to save:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFileChooserButton" id="save_to_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="action">select-folder</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">7</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
@@ -1807,7 +1875,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="margin-bottom">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="manager_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="visible" bind-source="manager_button" bind-property="active">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Picons manager</property>
|
||||
<attributes>
|
||||
@@ -1822,6 +1890,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="downloader_label">
|
||||
<property name="visible" bind-source="downloader_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Picons download tool</property>
|
||||
<attributes>
|
||||
@@ -1836,8 +1905,9 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="converter_label">
|
||||
<property name="visible" bind-source="converter_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Converter between name formats</property>
|
||||
<property name="label" translatable="yes">Converter between formats</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -40,11 +40,11 @@ from app.commons import run_idle, run_task, run_with_delay, log
|
||||
from app.connections import upload_data, DownloadType, download_data, remove_picons
|
||||
from app.settings import SettingsType, Settings, SEP, IS_DARWIN
|
||||
from app.tools.picons import (PiconsParser, parse_providers, Provider, convert_to, download_picon, PiconsCzDownloader,
|
||||
PiconsError)
|
||||
PiconsError, PiconFormat)
|
||||
from app.tools.satellites import SatellitesParser, SatelliteSource
|
||||
from .dialogs import show_dialog, DialogType, translate, get_builder, get_chooser_dialog
|
||||
from .main_helper import (scroll_to, on_popup_menu, get_base_model, set_picon, get_picon_pixbuf, get_picon_dialog,
|
||||
get_picon_file_name, get_pixbuf_from_data, get_pixbuf_at_scale)
|
||||
get_picon_file_name, get_pixbuf_from_data, get_pixbuf_at_scale, get_pos_num)
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey, Page, ViewTarget
|
||||
|
||||
|
||||
@@ -153,13 +153,9 @@ class PiconManager(Gtk.Box):
|
||||
self._bouquet_filter_switch = builder.get_object("bouquet_filter_switch")
|
||||
self._providers_header_box = builder.get_object("providers_header_box")
|
||||
self._header_download_box = builder.get_object("header_download_box")
|
||||
self._satellite_label.bind_property("visible", builder.get_object("loading_data_label"), "visible", 4)
|
||||
self._satellite_label.bind_property("visible", builder.get_object("loading_data_spinner"), "visible", 4)
|
||||
self._satellite_label.bind_property("visible", self._download_source_button, "sensitive")
|
||||
self._satellite_label.bind_property("visible", self._satellites_view, "sensitive")
|
||||
self._cancel_button.bind_property("visible", self._header_download_box, "visible", 4)
|
||||
self._convert_button.bind_property("visible", self._header_download_box, "visible", 4)
|
||||
self._download_source_button.bind_property("visible", self._receive_button, "visible")
|
||||
self._converter_sc_button = builder.get_object("converter_sc_button")
|
||||
self._converter_nt_button = builder.get_object("converter_nt_button")
|
||||
self._converter_bq_button = builder.get_object("converter_bq_button")
|
||||
# Info.
|
||||
self._dst_count_label = builder.get_object("dst_count_label")
|
||||
self._info_check_button = builder.get_object("info_check_button")
|
||||
@@ -169,24 +165,11 @@ class PiconManager(Gtk.Box):
|
||||
self._filter_bar = builder.get_object("filter_bar")
|
||||
self._auto_filter_switch = builder.get_object("auto_filter_switch")
|
||||
self._filter_button = builder.get_object("filter_button")
|
||||
self._filter_button.bind_property("active", self._filter_bar, "visible")
|
||||
self._filter_button.bind_property("active", self._src_filter_button, "visible")
|
||||
self._filter_button.bind_property("active", self._dst_filter_button, "visible")
|
||||
self._filter_button.bind_property("visible", self._info_check_button, "visible")
|
||||
self._filter_button.bind_property("visible", self._remove_button, "visible")
|
||||
self._src_button = builder.get_object("src_button")
|
||||
self._src_button.bind_property("active", builder.get_object("explorer_dst_label"), "visible")
|
||||
self._src_button.bind_property("active", builder.get_object("src_picon_box_frame"), "visible")
|
||||
self._filter_button.bind_property("visible", self._src_button, "visible")
|
||||
self._info_check_button.bind_property("active", builder.get_object("explorer_info_box_frame"), "visible")
|
||||
# Header buttons. -> Used instead stack switcher.
|
||||
self._manager_button = builder.get_object("manager_button")
|
||||
self._manager_button.bind_property("active", builder.get_object("manager_label"), "visible")
|
||||
self._downloader_button = builder.get_object("downloader_button")
|
||||
self._downloader_button.bind_property("active", builder.get_object("downloader_label"), "visible")
|
||||
self._converter_button = builder.get_object("converter_button")
|
||||
self._converter_button.bind_property("active", builder.get_object("converter_label"), "visible")
|
||||
self._manager_button.bind_property("active", builder.get_object("add_menu_button"), "visible")
|
||||
# Init drag-and-drop
|
||||
self.init_drag_and_drop()
|
||||
# Rendering.
|
||||
@@ -223,6 +206,8 @@ class PiconManager(Gtk.Box):
|
||||
name = "downloader"
|
||||
elif is_converter:
|
||||
name = "converter"
|
||||
if not self._enigma2_path_button.get_filename():
|
||||
self._enigma2_path_button.set_filename(self._settings.profile_picons_path)
|
||||
|
||||
self._stack.set_visible_child_name(name)
|
||||
|
||||
@@ -252,6 +237,7 @@ class PiconManager(Gtk.Box):
|
||||
def on_profile_changed(self, app, data):
|
||||
self._current_path_label.set_text(self._settings.profile_picons_path)
|
||||
self.update_picons_dest(app, self._app.page)
|
||||
self._enigma2_path_button.set_filename(self._settings.profile_picons_path)
|
||||
|
||||
def on_picon_assign(self, app, target):
|
||||
if target is ViewTarget.SERVICES:
|
||||
@@ -680,7 +666,7 @@ class PiconManager(Gtk.Box):
|
||||
model.clear()
|
||||
|
||||
try:
|
||||
for sat in sorted(sats):
|
||||
for sat in sorted(sats, key=lambda s: get_pos_num(s[1]), reverse=True):
|
||||
pos = sat[1]
|
||||
name = f"{sat[0]} ({pos})"
|
||||
if is_filter and pos not in self._sat_positions:
|
||||
@@ -825,9 +811,10 @@ class PiconManager(Gtk.Box):
|
||||
services = self._app.current_services
|
||||
|
||||
ids = set()
|
||||
for s in (services.get(fav_id) for fav_id in fav_bouquet):
|
||||
ids.add(s.picon_id)
|
||||
ids.add(get_picon_file_name(s.service))
|
||||
for s in (services.get(fav_id, None) for fav_id in fav_bouquet):
|
||||
if s:
|
||||
ids.add(s.picon_id)
|
||||
ids.add(get_picon_file_name(s.service))
|
||||
return ids
|
||||
|
||||
def process_provider(self, prv, picons_path):
|
||||
@@ -989,11 +976,10 @@ class PiconManager(Gtk.Box):
|
||||
return True
|
||||
|
||||
def on_tree_view_key_press(self, view, event):
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
key = KeyboardKey(event.hardware_keycode)
|
||||
if key is KeyboardKey.UNDEFINED:
|
||||
return
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
if key is KeyboardKey.DELETE:
|
||||
self.on_local_remove(view)
|
||||
|
||||
@@ -1018,9 +1004,25 @@ class PiconManager(Gtk.Box):
|
||||
return
|
||||
|
||||
self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(True))
|
||||
convert_to(src_path=picons_path,
|
||||
dest_path=save_path,
|
||||
s_type=SettingsType.ENIGMA_2,
|
||||
ids = None
|
||||
p_format = PiconFormat.NEUTRINO if self._converter_nt_button.get_active() else PiconFormat.OSCAM
|
||||
|
||||
if p_format is PiconFormat.OSCAM:
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError as e:
|
||||
self.show_info_message(f"{translate('Conversion error.')} {e}", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
if self._converter_bq_button.get_active():
|
||||
bq_selected = self._app.check_bouquet_selection()
|
||||
if not bq_selected:
|
||||
return
|
||||
|
||||
services = self._app.current_services
|
||||
ids = {services.get(s).picon_id for s in self._app.current_bouquets.get(bq_selected) if s in services}
|
||||
|
||||
convert_to(src_path=picons_path, dest_path=save_path, p_format=p_format, ids=ids, services=self._services,
|
||||
done_callback=lambda: self.show_info_message(translate("Done!"), Gtk.MessageType.INFO))
|
||||
|
||||
@run_idle
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2024 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
|
||||
@@ -158,7 +158,14 @@ class PlayerBox(Gtk.Overlay):
|
||||
return
|
||||
|
||||
ref = self._app.get_service_ref_data(srv)
|
||||
self.zap(ref, self.play_current)
|
||||
if mode is PlaybackMode.PLAY:
|
||||
self.play_service(ref)
|
||||
elif mode is PlaybackMode.ZAP:
|
||||
self.zap(ref)
|
||||
elif mode is PlaybackMode.ZAP_PLAY:
|
||||
self.zap(ref, self.play_current)
|
||||
elif mode is PlaybackMode.STREAM:
|
||||
self._app.show_error_message("Not allowed in this context!")
|
||||
|
||||
def on_iptv_clicked(self, app, mode):
|
||||
if not self._app.http_api:
|
||||
@@ -383,7 +390,7 @@ class PlayerBox(Gtk.Overlay):
|
||||
width, height = size
|
||||
|
||||
if self._playback_window:
|
||||
self._playback_window.show()
|
||||
self._playback_window.present()
|
||||
self._playback_window.set_title(title or self.get_playback_title())
|
||||
else:
|
||||
self._playback_window = Gtk.Window(title=title or self.get_playback_title(),
|
||||
@@ -439,15 +446,17 @@ class PlayerBox(Gtk.Overlay):
|
||||
self.play(url) if url else self.on_error(None, "No reference is present!")
|
||||
|
||||
def on_play_service(self, item=None):
|
||||
""" Playback without switching channel on the Box [returns current reference]"""
|
||||
""" Playback without switching channel on the Box."""
|
||||
ref, path = self.get_ref()
|
||||
if not ref:
|
||||
return
|
||||
|
||||
self.play_service(ref)
|
||||
|
||||
def play_service(self, ref):
|
||||
s_type = self._app.app_settings.setting_type
|
||||
req = HttpAPI.Request.STREAM if s_type is SettingsType.ENIGMA_2 else HttpAPI.Request.N_STREAM
|
||||
self._app.http_api.send(req, ref, self.watch)
|
||||
return ref
|
||||
|
||||
def on_zap(self, callback=None):
|
||||
""" Switch(zap) the channel. """
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2025 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
|
||||
@@ -143,7 +143,8 @@ class RecordingsTool(Gtk.Box):
|
||||
if self._ftp:
|
||||
self._ftp.close()
|
||||
|
||||
self._ftp = UtfFTP(host=self._settings.host, user=self._settings.user, passwd=self._settings.password)
|
||||
host, port = self._settings.host, self._settings.port
|
||||
self._ftp = UtfFTP(host=host, port=port, user=self._settings.user, passwd=self._settings.password)
|
||||
self._ftp.encoding = "utf-8"
|
||||
except all_errors:
|
||||
pass # NOP
|
||||
@@ -303,11 +304,10 @@ class RecordingsTool(Gtk.Box):
|
||||
self._filter_entry.grab_focus() if button.get_active() else self._filter_entry.set_text("")
|
||||
|
||||
def on_recordings_key_press(self, view, event):
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
key = KeyboardKey(event.hardware_keycode)
|
||||
if key is KeyboardKey.UNDEFINED:
|
||||
return
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
if key is KeyboardKey.DELETE:
|
||||
self.on_recording_remove()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2026 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
|
||||
@@ -32,7 +32,7 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2026 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkListStore" id="fec_list_store">
|
||||
<columns>
|
||||
@@ -267,6 +267,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="title" translatable="yes">Service data</property>
|
||||
<property name="resizable">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="width-request">800</property>
|
||||
<property name="window-position">center-on-parent</property>
|
||||
<property name="destroy-with-parent">True</property>
|
||||
<property name="icon-name">document-properties-symbolic</property>
|
||||
@@ -289,7 +290,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Save current service</property>
|
||||
<property name="tooltip_text" translatable="yes">Save current changes</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_save" swapped="no"/>
|
||||
<accelerator key="Return" signal="activate"/>
|
||||
@@ -297,12 +298,13 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="create_button">
|
||||
<property name="label" translatable="yes">Create</property>
|
||||
<property name="label" translatable="yes">Add</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Create and save as new service</property>
|
||||
<property name="tooltip_text" translatable="yes">Create a new service</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_create_new" swapped="no"/>
|
||||
<signal name="clicked" handler="on_save" swapped="no"/>
|
||||
<accelerator key="Return" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -38,16 +38,16 @@ from app.eparser.ecommons import (MODULATION, Inversion, ROLL_OFF, Pilot, Flag,
|
||||
from app.eparser.neutrino import get_attributes, SP, KSP
|
||||
from app.settings import SettingsType
|
||||
from .dialogs import show_dialog, DialogType, Action, get_builder
|
||||
from .main_helper import get_base_model
|
||||
from .main_helper import get_base_model, scroll_to
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, CODED_ICON, Column
|
||||
|
||||
_UI_PATH = UI_RESOURCES_PATH + "service_details_dialog.glade"
|
||||
_UI_PATH = f"{UI_RESOURCES_PATH}service_dialog.glade"
|
||||
|
||||
|
||||
class ServiceDetailsDialog:
|
||||
_ENIGMA2_DATA_ID = "{:04x}:{:08x}:{:04x}:{:04x}:{}:{}"
|
||||
|
||||
_ENIGMA2_FAV_ID = "{:X}:{:X}:{:X}:{:X}"
|
||||
_ENIGMA2_FAV_ID = "1:0:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0"
|
||||
|
||||
_ENIGMA2_TRANSPONDER_DATA = "{} {}:{}:{}:{}:{}:{}:{}"
|
||||
|
||||
@@ -62,10 +62,9 @@ class ServiceDetailsDialog:
|
||||
|
||||
_DIGIT_ENTRY_NAME = "digit-entry"
|
||||
|
||||
def __init__(self, app, new_color, action=Action.EDIT):
|
||||
def __init__(self, app, action=Action.EDIT, tr_type=TrType.Satellite):
|
||||
handlers = {"on_system_changed": self.on_system_changed,
|
||||
"on_save": self.on_save,
|
||||
"on_create_new": self.on_create_new,
|
||||
"on_tr_edit_toggled": self.on_tr_edit_toggled,
|
||||
"update_reference": self.update_reference,
|
||||
"on_cas_entry_changed": self.on_cas_entry_changed,
|
||||
@@ -81,7 +80,7 @@ class ServiceDetailsDialog:
|
||||
self._dialog = builder.get_object("service_details_dialog")
|
||||
self._dialog.set_transient_for(app.app_window)
|
||||
self._s_type = settings.setting_type
|
||||
self._tr_type = TrType.Satellite
|
||||
self._tr_type = tr_type
|
||||
self._picons_path = settings.profile_picons_path
|
||||
self._services_view = app.services_view
|
||||
self._fav_view = app.fav_view
|
||||
@@ -89,19 +88,19 @@ class ServiceDetailsDialog:
|
||||
self._old_service = None
|
||||
self._services = app.current_services
|
||||
self._bouquets = app.current_bouquets
|
||||
self._new_color = new_color
|
||||
self._new_color = app._NEW_COLOR
|
||||
self._transponder_services_iters = None
|
||||
self._current_model = None
|
||||
self._current_itr = None
|
||||
# Patterns
|
||||
# Patterns.
|
||||
self._DIGIT_PATTERN = re.compile("\\D")
|
||||
self._NON_EMPTY_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
|
||||
self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-fA-F]{1,4})(,C:[0-9a-fA-F]{1,4})*")
|
||||
self._PIDS_PATTERN = re.compile("(?:^[\\s]*$)|(c:[0-9]{2}[0-9a-fA-F]{1,4})(,c:[0-9]{2}[0-9a-fA-F]{1,4})*?")
|
||||
# Buttons
|
||||
# Buttons.
|
||||
self._apply_button = builder.get_object("apply_button")
|
||||
self._create_button = builder.get_object("create_button")
|
||||
# style
|
||||
# Style.
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
# initialization only digit elements
|
||||
@@ -175,21 +174,53 @@ class ServiceDetailsDialog:
|
||||
def show(self):
|
||||
self._dialog.show()
|
||||
|
||||
@run_idle
|
||||
def init_default_data_elements(self):
|
||||
srv_data = [None] * 20
|
||||
srv_data[Column.SRV_CAS_FLAGS] = "f:40"
|
||||
srv_data[Column.SRV_SERVICE] = "New"
|
||||
srv_data[Column.SRV_PACKAGE] = "New"
|
||||
srv_data[Column.SRV_SSID] = "0"
|
||||
srv_data[Column.SRV_PICON_ID] = "1_0_1_0_0_0_000000_0_0_0.png"
|
||||
srv_data[Column.SRV_FAV_ID] = "1:0:1:0:0:0:000000:0:0:0::0:0:0:0"
|
||||
|
||||
if self._tr_type is TrType.Cable:
|
||||
srv_data[Column.SRV_STANDARD] = "c"
|
||||
srv_data[Column.SRV_FREQ] = "300"
|
||||
srv_data[Column.SRV_RATE] = "6000"
|
||||
srv_data[Column.SRV_SYSTEM] = "DVB-C"
|
||||
srv_data[Column.SRV_POS] = "C"
|
||||
srv_data[Column.SRV_DATA_ID] = "0000:00000000:0:0:1:0"
|
||||
srv_data[Column.SRV_TRANSPONDER] = "t 300000000:0:0:0:0:0:0:0:0:0:0:0"
|
||||
elif self._tr_type is TrType.Terrestrial:
|
||||
srv_data[Column.SRV_STANDARD] = "t"
|
||||
srv_data[Column.SRV_FREQ] = "420000"
|
||||
srv_data[Column.SRV_RATE] = "0"
|
||||
srv_data[Column.SRV_SYSTEM] = "DVB-T2"
|
||||
srv_data[Column.SRV_POS] = "T"
|
||||
srv_data[Column.SRV_DATA_ID] = "0000:00000000:0:0:1:0"
|
||||
srv_data[Column.SRV_TRANSPONDER] = "t 420000000:0:5:5:3:2:4:4:2:1:0:0"
|
||||
else:
|
||||
srv_data[Column.SRV_STANDARD] = "s"
|
||||
srv_data[Column.SRV_FREQ] = "10720"
|
||||
srv_data[Column.SRV_RATE] = "27500"
|
||||
srv_data[Column.SRV_POL] = "H"
|
||||
srv_data[Column.SRV_FEC] = "Auto"
|
||||
srv_data[Column.SRV_SYSTEM] = "DVB-S"
|
||||
srv_data[Column.SRV_POS] = "0.0E"
|
||||
srv_data[Column.SRV_DATA_ID] = "0:00000000:0:0000:1:0:0:0:0:0"
|
||||
srv_data[Column.SRV_TRANSPONDER] = "s 10720000:27500000:0:1:0:0:0:0:0"
|
||||
|
||||
srv = Service(*srv_data)
|
||||
|
||||
self._old_service = srv
|
||||
self._apply_button.set_visible(False)
|
||||
self._create_button.set_visible(True)
|
||||
self._tr_edit_switch.set_sensitive(False)
|
||||
self.on_tr_edit_toggled(self._tr_edit_switch.set_active(True), True)
|
||||
for elem in self._non_empty_elements.values():
|
||||
elem.set_text(" ")
|
||||
elem.set_text("")
|
||||
self._new_check_button.set_active(True)
|
||||
self._service_type_combo_box.set_active(0)
|
||||
self._pol_combo_box.set_active(0)
|
||||
self._fec_combo_box.set_active(0)
|
||||
self._sys_combo_box.set_active(0)
|
||||
self._invertion_combo_box.set_active(2)
|
||||
|
||||
self.init_service_data(srv)
|
||||
|
||||
self._current_model = get_base_model(self._services_view.get_model())
|
||||
|
||||
def update_data_elements(self):
|
||||
model, paths = self._services_view.get_selection().get_selected_rows()
|
||||
@@ -215,6 +246,9 @@ class ServiceDetailsDialog:
|
||||
srv = Service(*self._current_model[itr][: Column.SRV_TOOLTIP])
|
||||
self._old_service = srv
|
||||
self._current_itr = itr
|
||||
self.init_service_data(srv)
|
||||
|
||||
def init_service_data(self, srv):
|
||||
# Service
|
||||
self._name_entry.set_text(srv.service)
|
||||
self._package_entry.set_text(srv.package)
|
||||
@@ -226,6 +260,15 @@ class ServiceDetailsDialog:
|
||||
self.select_active_text(self._pol_combo_box, srv.pol)
|
||||
self.select_active_text(self._fec_combo_box, srv.fec)
|
||||
self.select_active_text(self._sys_combo_box, srv.system)
|
||||
self.update_ui(srv)
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self.init_enigma2_service_data(srv)
|
||||
self.init_enigma2_transponder_data(srv)
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
self.init_neutrino_data(srv)
|
||||
self.init_neutrino_ui_elements()
|
||||
|
||||
def update_ui(self, srv):
|
||||
if self._tr_type is TrType.Terrestrial:
|
||||
self.update_ui_for_terrestrial()
|
||||
elif self._tr_type is TrType.Cable:
|
||||
@@ -235,13 +278,6 @@ class ServiceDetailsDialog:
|
||||
else:
|
||||
self.set_sat_positions(srv.pos)
|
||||
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self.init_enigma2_service_data(srv)
|
||||
self.init_enigma2_transponder_data(srv)
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
self.init_neutrino_data(srv)
|
||||
self.init_neutrino_ui_elements()
|
||||
|
||||
# ***************** Init Enigma2 data *********************#
|
||||
|
||||
@run_idle
|
||||
@@ -303,6 +339,7 @@ class ServiceDetailsDialog:
|
||||
data = srv.data_id.split(":")
|
||||
tr_data = srv.transponder.split(":")
|
||||
tr_type = TrType(srv.transponder_type)
|
||||
data_len = len(tr_data)
|
||||
|
||||
self._namespace_entry.set_text(str(int(data[1], 16)))
|
||||
self._transponder_id_entry.set_text(str(int(data[2], 16)))
|
||||
@@ -311,11 +348,12 @@ class ServiceDetailsDialog:
|
||||
if tr_type is TrType.Satellite:
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[5]).name)
|
||||
if srv.system == "DVB-S2":
|
||||
self.select_active_text(self._mod_combo_box, MODULATION.get(tr_data[8]))
|
||||
self.select_active_text(self._rolloff_combo_box, ROLL_OFF.get(tr_data[9]))
|
||||
self.select_active_text(self._pilot_combo_box, Pilot(tr_data[10]).name)
|
||||
self._tr_flag_entry.set_text(tr_data[7])
|
||||
if len(tr_data) > 12:
|
||||
if data_len > 9:
|
||||
self.select_active_text(self._mod_combo_box, MODULATION.get(tr_data[8]))
|
||||
self.select_active_text(self._rolloff_combo_box, ROLL_OFF.get(tr_data[9]))
|
||||
self.select_active_text(self._pilot_combo_box, Pilot(tr_data[10]).name)
|
||||
self._tr_flag_entry.set_text(tr_data[6])
|
||||
if data_len > 12:
|
||||
self._stream_id_entry.set_text(tr_data[11])
|
||||
self._pls_code_entry.set_text(tr_data[12])
|
||||
self.select_active_text(self._pls_mode_combo_box, PLS_MODE.get(tr_data[13]))
|
||||
@@ -373,6 +411,12 @@ class ServiceDetailsDialog:
|
||||
""" Sat positions initialisation """
|
||||
self._sat_pos_button.set_value(float(sat_pos[:-1]))
|
||||
self._pos_side_box.set_active_id(sat_pos[-1:])
|
||||
self._sat_pos_button.connect("value-changed", self.on_sat_value_changed)
|
||||
|
||||
def on_sat_value_changed(self, button):
|
||||
pos = int(self.get_sat_position())
|
||||
namespace = int(f"{3600 - abs(pos) if pos < 0 else pos:04x}0000", 16)
|
||||
self._namespace_entry.set_text(str(namespace))
|
||||
|
||||
def on_system_changed(self, box):
|
||||
if not self._tr_edit_switch.get_active():
|
||||
@@ -401,11 +445,8 @@ class ServiceDetailsDialog:
|
||||
def on_save(self, item):
|
||||
self.save_data()
|
||||
|
||||
def on_create_new(self, item):
|
||||
self.save_data()
|
||||
|
||||
def save_data(self):
|
||||
if self._s_type is SettingsType.NEUTRINO_MP and self._tr_type is not TrType.Satellite:
|
||||
if self._s_type is SettingsType.NEUTRINO_MP:
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
|
||||
return
|
||||
|
||||
@@ -421,12 +462,25 @@ class ServiceDetailsDialog:
|
||||
|
||||
def on_new(self):
|
||||
""" Create new service. """
|
||||
service = self.get_service(*self.get_srv_data(), self.get_satellite_transponder_data())
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
|
||||
srv_data = self.update_service_data()
|
||||
if not srv_data:
|
||||
return False
|
||||
|
||||
service, data = srv_data
|
||||
itr = self._current_model.append(service + (None, data.get(Column.SRV_BACKGROUND, None)))
|
||||
scroll_to(self._current_model.get_path(itr), self._services_view)
|
||||
|
||||
return True
|
||||
|
||||
def on_edit(self):
|
||||
""" Edit current service. """
|
||||
service, extra_data = self.update_service_data()
|
||||
self._current_model.set(self._current_itr, extra_data)
|
||||
self._current_model.set(self._current_itr, {i: v for i, v in enumerate(service)})
|
||||
self.update_fav_view(self._old_service, service)
|
||||
return True
|
||||
|
||||
def update_service_data(self):
|
||||
fav_id, data_id = self.get_srv_data()
|
||||
# Transponder
|
||||
transponder = self._old_service.transponder
|
||||
@@ -441,8 +495,9 @@ class ServiceDetailsDialog:
|
||||
elif self._tr_type is TrType.ATSC:
|
||||
transponder = self.get_atsc_transponder_data()
|
||||
except Exception as e:
|
||||
log("Edit service error: {}".format(e))
|
||||
log(f"Edit service error: {e}")
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Error getting transponder parameters!")
|
||||
return False
|
||||
else:
|
||||
if self._transponder_services_iters:
|
||||
self.update_transponder_services(transponder, self.get_sat_position())
|
||||
@@ -468,11 +523,9 @@ class ServiceDetailsDialog:
|
||||
if f_flags and Flag.is_new(Flag.parse(f_flags[0])):
|
||||
extra_data[Column.SRV_BACKGROUND] = self._new_color
|
||||
|
||||
self._current_model.set(self._current_itr, extra_data)
|
||||
self._current_model.set(self._current_itr, {i: v for i, v in enumerate(service)})
|
||||
self.update_fav_view(self._old_service, service)
|
||||
self._old_service = service
|
||||
return True
|
||||
|
||||
return service, extra_data
|
||||
|
||||
def update_bouquets(self, fav_id, old_fav_id):
|
||||
self._services.pop(old_fav_id, None)
|
||||
@@ -593,7 +646,7 @@ class ServiceDetailsDialog:
|
||||
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)
|
||||
fav_id = f"{self._reference_label.get_text()}:"
|
||||
return fav_id, data_id
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
data = get_attributes(self._old_service.data_id)
|
||||
@@ -615,7 +668,7 @@ class ServiceDetailsDialog:
|
||||
freq = self._freq_entry.get_text()
|
||||
rate = self._rate_entry.get_text()
|
||||
pol = self._pol_combo_box.get_active_id()
|
||||
pos = "{}{}".format(round(self._sat_pos_button.get_value(), 1), self._pos_side_box.get_active_id())
|
||||
pos = f"{round(self._sat_pos_button.get_value(), 1)}{self._pos_side_box.get_active_id()}"
|
||||
return freq, rate, pol, fec, system, pos
|
||||
elif self._tr_type in (TrType.Terrestrial, TrType.ATSC):
|
||||
return freq, o_srv.rate, o_srv.pol, fec, system, o_srv.pos
|
||||
@@ -624,30 +677,30 @@ class ServiceDetailsDialog:
|
||||
|
||||
def get_satellite_transponder_data(self):
|
||||
sys = self._sys_combo_box.get_active_id()
|
||||
freq = "{}000".format(self._freq_entry.get_text())
|
||||
rate = "{}000".format(self._rate_entry.get_text())
|
||||
freq = f"{self._freq_entry.get_text()}000"
|
||||
rate = f"{self._rate_entry.get_text()}000"
|
||||
pol = self.get_value_from_combobox_id(self._pol_combo_box, POLARIZATION)
|
||||
fec = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
|
||||
sat_pos = self.get_sat_position()
|
||||
|
||||
inv = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
srv_sys = "0" # !!!
|
||||
flag = self._tr_flag_entry.get_text() or "0"
|
||||
|
||||
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)
|
||||
dvb_s_tr = self._ENIGMA2_TRANSPONDER_DATA.format("s", freq, rate, pol, fec, sat_pos, inv, flag)
|
||||
if sys == "DVB-S":
|
||||
return dvb_s_tr
|
||||
if sys == "DVB-S2":
|
||||
flag = self._tr_flag_entry.get_text()
|
||||
mod = self.get_value_from_combobox_id(self._mod_combo_box, MODULATION)
|
||||
roll_off = self.get_value_from_combobox_id(self._rolloff_combo_box, ROLL_OFF)
|
||||
pilot = get_value_by_name(Pilot, self._pilot_combo_box.get_active_id())
|
||||
pls_mode = self.get_value_from_combobox_id(self._pls_mode_combo_box, PLS_MODE)
|
||||
pls_code = self._pls_code_entry.get_text()
|
||||
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 ""
|
||||
pls = f":{st_id}:{pls_code}:{pls_mode}" if pls_mode and pls_code and st_id else ""
|
||||
|
||||
return f"{dvb_s_tr}:1:{mod}:{roll_off}:{pilot}{pls}"
|
||||
|
||||
return "{}:{}:{}:{}:{}{}".format(dvb_s_tr, flag, mod, roll_off, pilot, pls)
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
tr_data = get_attributes(self._old_service.transponder)
|
||||
tr_data["frq"] = freq
|
||||
@@ -658,7 +711,7 @@ class ServiceDetailsDialog:
|
||||
tr_data["id"] = "{:04x}".format(int(self._transponder_id_entry.get_text()))
|
||||
tr_data["inv"] = inv
|
||||
|
||||
return SP.join("{}{}{}".format(k, KSP, v) for k, v in tr_data.items())
|
||||
return SP.join(f"{k}{KSP}{v}" for k, v in tr_data.items())
|
||||
|
||||
def get_sat_position(self):
|
||||
sat_pos = self._sat_pos_button.get_value() * (-1 if self._pos_side_box.get_active_id() == "W" else 1)
|
||||
@@ -666,11 +719,11 @@ class ServiceDetailsDialog:
|
||||
return sat_pos
|
||||
|
||||
def get_terrestrial_transponder_data(self):
|
||||
tr_data = re.split("\s|:", self._old_service.transponder)
|
||||
tr_data = re.split(r"\s|:", self._old_service.transponder)
|
||||
# frequency, bandwidth, code rate HP, code rate LP, modulation, transmission mode, guard interval, hierarchy,
|
||||
# inversion, system, plp_id
|
||||
# Bandwidth -> Pol, Rate HP -> FEC, TransmissionMode -> Roll off, GuardInterval -> Pilot, Hierarchy -> Pls Mode
|
||||
tr_data[1] = "{}000".format(self._freq_entry.get_text())
|
||||
tr_data[1] = f"{self._freq_entry.get_text()}000"
|
||||
tr_data[2] = self.get_value_from_combobox_id(self._pol_combo_box, BANDWIDTH)
|
||||
tr_data[3] = self.get_value_from_combobox_id(self._fec_combo_box, T_FEC)
|
||||
tr_data[4] = self.get_value_from_combobox_id(self._rate_lp_combo_box, T_FEC)
|
||||
@@ -681,28 +734,28 @@ class ServiceDetailsDialog:
|
||||
tr_data[9] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
tr_data[10] = self.get_value_from_combobox_id(self._sys_combo_box, T_SYSTEM)
|
||||
|
||||
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
|
||||
return f"{tr_data[0]} {':'.join(tr_data[1:])}"
|
||||
|
||||
def get_cable_transponder_data(self):
|
||||
tr_data = re.split("\s|:", self._old_service.transponder)
|
||||
tr_data = re.split(r"\s|:", self._old_service.transponder)
|
||||
# frequency, symbol_rate, modulation, inversion, fec_inner, system;
|
||||
tr_data[1] = "{}000".format(self._freq_entry.get_text())
|
||||
tr_data[2] = "{}000".format(self._rate_entry.get_text())
|
||||
tr_data[1] = f"{self._freq_entry.get_text()}000"
|
||||
tr_data[2] = f"{self._rate_entry.get_text()}000"
|
||||
tr_data[3] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
tr_data[4] = self.get_value_from_combobox_id(self._mod_combo_box, C_MODULATION)
|
||||
tr_data[5] = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
|
||||
tr_data[6] = get_value_by_name(SystemCable, self._sys_combo_box.get_active_id())
|
||||
|
||||
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
|
||||
return f"{tr_data[0]} {':'.join(tr_data[1:])}"
|
||||
|
||||
def get_atsc_transponder_data(self):
|
||||
tr_data = re.split("\s|:", self._old_service.transponder)
|
||||
tr_data = re.split(r"\s|:", self._old_service.transponder)
|
||||
# frequency, inversion, modulation, system
|
||||
tr_data[1] = "{}000".format(self._freq_entry.get_text())
|
||||
tr_data[1] = f"{self._freq_entry.get_text()}000"
|
||||
tr_data[2] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
tr_data[3] = self.get_value_from_combobox_id(self._mod_combo_box, A_MODULATION)
|
||||
|
||||
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
|
||||
return f"{tr_data[0]} {':'.join(tr_data[1:])}"
|
||||
|
||||
def update_transponder_services(self, transponder, sat_pos):
|
||||
for itr in self._transponder_services_iters:
|
||||
@@ -714,13 +767,13 @@ class ServiceDetailsDialog:
|
||||
fav_id = srv[Column.SRV_FAV_ID]
|
||||
old_srv = self._services.pop(fav_id, None)
|
||||
if not old_srv:
|
||||
log("Update transponder services error: No service found for ID {}".format(srv[Column.SRV_FAV_ID]))
|
||||
log(f"Update transponder services error: No service found for ID {srv[Column.SRV_FAV_ID]}")
|
||||
continue
|
||||
|
||||
if self._s_type is SettingsType.NEUTRINO_MP:
|
||||
flags = get_attributes(srv[Column.SRV_CAS_FLAGS])
|
||||
flags["position"] = sat_pos
|
||||
srv[Column.SRV_CAS_FLAGS] = SP.join("{}{}{}".format(k, KSP, v) for k, v in flags.items())
|
||||
srv[Column.SRV_CAS_FLAGS] = SP.join(f"{k}{KSP}{v}" for k, v in flags.items())
|
||||
|
||||
self._services[fav_id] = Service(*srv[:Column.SRV_TOOLTIP])
|
||||
self._current_model.set_row(itr, srv)
|
||||
@@ -794,10 +847,9 @@ class ServiceDetailsDialog:
|
||||
nid = int(self._network_id_entry.get_text())
|
||||
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_label.set_text(ref)
|
||||
self._reference_label.set_text(self._ENIGMA2_FAV_ID.format(srv_type, ssid, tid, nid, on_id))
|
||||
else:
|
||||
self._reference_label.set_text("{:x}{:04x}{:04x}".format(tid, nid, ssid))
|
||||
self._reference_label.set_text(f"{tid:x}{nid:04x}{ssid:04x}")
|
||||
|
||||
def update_ui_for_terrestrial(self):
|
||||
tr_grid = self.get_transponder_grid_for_non_satellite()
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2
|
||||
|
||||
Copyright (C) 2018-2023 Dmitriy Yefremov
|
||||
Copyright (C) 2018-2025 Dmitriy Yefremov
|
||||
|
||||
Copying and distribution of this file, with or without modification,
|
||||
are permitted in any medium without royalty provided the copyright
|
||||
@@ -17,7 +17,7 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-license-type all_permissive -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor. -->
|
||||
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2025 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkAdjustment" id="font_size_adjustment">
|
||||
<property name="lower">8</property>
|
||||
@@ -119,6 +119,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="modal">True</property>
|
||||
<property name="window-position">center-on-parent</property>
|
||||
<property name="destroy-with-parent">True</property>
|
||||
<property name="icon-name">demon-editor</property>
|
||||
<property name="type-hint">dialog</property>
|
||||
<property name="skip-taskbar-hint">True</property>
|
||||
<property name="skip-pager-hint">True</property>
|
||||
@@ -838,6 +839,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="max-width-chars">6</property>
|
||||
<property name="text">21</property>
|
||||
<property name="primary-icon-name">network-workgroup-symbolic</property>
|
||||
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
@@ -860,6 +862,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="max-width-chars">6</property>
|
||||
<property name="text" translatable="yes">80</property>
|
||||
<property name="primary-icon-name">network-workgroup-symbolic</property>
|
||||
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -905,6 +908,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="max-width-chars">6</property>
|
||||
<property name="text" translatable="yes">23</property>
|
||||
<property name="primary-icon-name">network-workgroup-symbolic</property>
|
||||
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -2414,6 +2418,7 @@ Author: Dmitriy Yefremov
|
||||
<item id="nl_NL">Nederlands</item>
|
||||
<item id="pl_PL">Polski</item>
|
||||
<item id="pt_PT">Português</item>
|
||||
<item id="sk_SK">Slovák</item>
|
||||
<item id="tr_TR">Türkçe</item>
|
||||
<item id="be_BY">Беларуская</item>
|
||||
<item id="ru_RU">Русский</item>
|
||||
@@ -3947,6 +3952,46 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="enable_epg_name_cache_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive" bind-source="enable_experimental_switch" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Enables additional cache to display EPG for some IPTV channels imported from *.m3u.</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="enable_epg_name_cache_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="label" translatable="yes">Enable additional name cache for EPG</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="enable_epg_name_cache_switch">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="halign">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -185,6 +185,7 @@ class SettingsDialog:
|
||||
self._enable_yt_dl_switch = builder.get_object("enable_yt_dl_switch")
|
||||
self._enable_update_yt_dl_switch = builder.get_object("enable_update_yt_dl_switch")
|
||||
self._enable_send_to_switch = builder.get_object("enable_send_to_switch")
|
||||
self._enable_epg_name_cache_switch = builder.get_object("enable_epg_name_cache_switch")
|
||||
self._enable_exp_switch = builder.get_object("enable_experimental_switch")
|
||||
# Profiles.
|
||||
self._profile_view = builder.get_object("profile_tree_view")
|
||||
@@ -268,7 +269,13 @@ class SettingsDialog:
|
||||
def on_response(self, dialog, resp):
|
||||
if resp == Gtk.ResponseType.ACCEPT:
|
||||
self._updated = self.on_save_settings()
|
||||
dialog.destroy()
|
||||
if not self._updated:
|
||||
return True
|
||||
|
||||
if resp == Gtk.ResponseType.DELETE_EVENT or resp == Gtk.ResponseType.ACCEPT:
|
||||
dialog.destroy()
|
||||
|
||||
return False
|
||||
|
||||
def on_field_button_press(self, entry):
|
||||
update_entry_data(entry, self._dialog, self._settings)
|
||||
@@ -290,12 +297,12 @@ class SettingsDialog:
|
||||
self._hosts_box.remove_all()
|
||||
self._remove_host_button.set_sensitive(len([self._hosts_box.append(h, h) for h in self._settings.hosts]) > 1)
|
||||
self._hosts_box.set_active_id(self._settings.host)
|
||||
self._port_field.set_text(self._settings.port)
|
||||
self._port_field.set_text(str(self._settings.port))
|
||||
self._login_field.set_text(self._settings.user)
|
||||
self._password_field.set_text(self._settings.password)
|
||||
self._http_port_field.set_text(self._settings.http_port)
|
||||
self._http_port_field.set_text(str(self._settings.http_port))
|
||||
self._http_use_ssl_check_button.set_active(self._settings.http_use_ssl)
|
||||
self._telnet_port_field.set_text(self._settings.telnet_port)
|
||||
self._telnet_port_field.set_text(str(self._settings.telnet_port))
|
||||
self._telnet_timeout_spin_button.set_value(self._settings.telnet_timeout)
|
||||
self._services_field.set_text(self._settings.services_path)
|
||||
self._user_bouquet_field.set_text(self._settings.user_bouquet_path)
|
||||
@@ -338,6 +345,7 @@ class SettingsDialog:
|
||||
self._enable_yt_dl_switch.set_active(self._settings.enable_yt_dl)
|
||||
self._enable_update_yt_dl_switch.set_active(self._settings.enable_yt_dl_update)
|
||||
self._enable_send_to_switch.set_active(self._settings.enable_send_to)
|
||||
self._enable_epg_name_cache_switch.set_active(self._settings.enable_epg_name_cache)
|
||||
self._set_color_switch.set_active(self._settings.use_colors)
|
||||
new_rgb = Gdk.RGBA()
|
||||
new_rgb.parse(self._settings.new_color)
|
||||
@@ -351,29 +359,34 @@ class SettingsDialog:
|
||||
def on_apply_profile_settings(self, item=None):
|
||||
if not self.is_data_correct(self._digit_elems):
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
return
|
||||
return False
|
||||
|
||||
self._s_type = SettingsType(int(self._settings_type_box.get_active_id()))
|
||||
self._settings.setting_type = self._s_type
|
||||
self._settings.host = self._host_field.get_text()
|
||||
self._settings.hosts = [h[1] for h in self._hosts_box.get_model()]
|
||||
self._settings.port = self._port_field.get_text()
|
||||
self._settings.port = int(self._port_field.get_text())
|
||||
self._settings.user = self._login_field.get_text()
|
||||
self._settings.password = self._password_field.get_text()
|
||||
self._settings.http_port = self._http_port_field.get_text()
|
||||
self._settings.http_port = int(self._http_port_field.get_text())
|
||||
self._settings.http_use_ssl = self._http_use_ssl_check_button.get_active()
|
||||
self._settings.telnet_port = self._telnet_port_field.get_text()
|
||||
self._settings.telnet_port = int(self._telnet_port_field.get_text())
|
||||
self._settings.telnet_timeout = int(self._telnet_timeout_spin_button.get_value())
|
||||
self._settings.services_path = self._services_field.get_text()
|
||||
self._settings.satellites_xml_path = self._satellites_xml_field.get_text()
|
||||
self._settings.user_bouquet_path = self._user_bouquet_field.get_text()
|
||||
self._settings.epg_dat_path = self._epg_dat_box.get_active_id()
|
||||
self._settings.picons_path = self._picons_paths_box.get_active_id()
|
||||
|
||||
return True
|
||||
|
||||
def on_save_settings(self, item=None):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
|
||||
return False
|
||||
|
||||
self.on_apply_profile_settings()
|
||||
if not self.on_apply_profile_settings():
|
||||
return False
|
||||
|
||||
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()
|
||||
@@ -420,6 +433,7 @@ class SettingsDialog:
|
||||
self._ext_settings.enable_yt_dl = self._enable_yt_dl_switch.get_active()
|
||||
self._ext_settings.enable_yt_dl_update = self._enable_update_yt_dl_switch.get_active()
|
||||
self._ext_settings.enable_send_to = self._enable_send_to_switch.get_active()
|
||||
self._ext_settings.enable_epg_name_cache = self._enable_epg_name_cache_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()
|
||||
@@ -430,6 +444,11 @@ class SettingsDialog:
|
||||
def on_connection_test(self, item):
|
||||
if self._test_spinner.get_state() is Gtk.StateType.ACTIVE:
|
||||
return
|
||||
|
||||
if not self.is_data_correct((self._port_field, self._http_port_field, self._telnet_port_field)):
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
return
|
||||
|
||||
self.show_spinner(True)
|
||||
if self._ftp_radio_button.get_active():
|
||||
self.test_ftp()
|
||||
@@ -440,7 +459,7 @@ class SettingsDialog:
|
||||
|
||||
def test_http(self):
|
||||
user, password = self._login_field.get_text(), self._password_field.get_text()
|
||||
host, port = self._host_field.get_text(), self._http_port_field.get_text()
|
||||
host, port = self._host_field.get_text(), int(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, use_ssl=use_ssl, s_type=self._s_type),
|
||||
@@ -454,7 +473,7 @@ class SettingsDialog:
|
||||
|
||||
def test_telnet(self):
|
||||
timeout = int(self._telnet_timeout_spin_button.get_value())
|
||||
host, port = self._host_field.get_text(), self._telnet_port_field.get_text()
|
||||
host, port = self._host_field.get_text(), int(self._telnet_port_field.get_text())
|
||||
user, password = self._login_field.get_text(), self._password_field.get_text()
|
||||
try:
|
||||
self.show_info_message(test_telnet(host, port, user, password, timeout), Gtk.MessageType.INFO)
|
||||
@@ -464,7 +483,7 @@ class SettingsDialog:
|
||||
self.show_spinner(False)
|
||||
|
||||
def test_ftp(self):
|
||||
host, port = self._host_field.get_text(), self._port_field.get_text()
|
||||
host, port = self._host_field.get_text(), int(self._port_field.get_text())
|
||||
user, password = self._login_field.get_text(), self._password_field.get_text()
|
||||
try:
|
||||
self.show_info_message(f"OK. {test_ftp(host, port, user, password)}", Gtk.MessageType.INFO)
|
||||
@@ -497,6 +516,7 @@ class SettingsDialog:
|
||||
self._support_ver5_switch.set_active(state)
|
||||
self._unlimited_buffer_switch.set_active(state)
|
||||
self._enable_send_to_switch.set_active(state)
|
||||
self._enable_epg_name_cache_switch.set_active(state)
|
||||
self._enable_yt_dl_switch.set_active(state)
|
||||
|
||||
def on_force_bq_name(self, switch, state):
|
||||
@@ -672,7 +692,7 @@ class SettingsDialog:
|
||||
if mode is PlaybackMode.PLAY:
|
||||
self.show_info_message("Operates in standby mode or current active transponder!", Gtk.MessageType.WARNING)
|
||||
elif mode is PlaybackMode.STREAM:
|
||||
self.show_info_message("Playback IPTV streams only!", Gtk.MessageType.WARNING)
|
||||
self.show_info_message("Playback of IPTV streams only!", Gtk.MessageType.WARNING)
|
||||
elif mode is PlaybackMode.DISABLED:
|
||||
self._allow_main_list_playback_switch.set_active(False)
|
||||
else:
|
||||
|
||||
@@ -49,6 +49,10 @@ paned.vertical > separator {
|
||||
background-size: 24px 2px;
|
||||
}
|
||||
|
||||
progressbar > trough {
|
||||
min-width: 75px;
|
||||
}
|
||||
|
||||
.red-button {
|
||||
background-image: none;
|
||||
background-color: red;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -24,6 +24,8 @@
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
from app.ui.dialogs import translate
|
||||
from .uicommons import Gtk, GLib
|
||||
|
||||
@@ -52,14 +54,13 @@ class BGTaskWidget(Gtk.Box):
|
||||
self.pack_start(close_button, False, False, 0)
|
||||
|
||||
self.show_all()
|
||||
|
||||
# Just prototype. -> It may not work properly!
|
||||
# TODO: Different options need to be tested. Possibly with normal threads.
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from gi.repository.Gio import Task, Cancellable
|
||||
|
||||
self._executor = ThreadPoolExecutor(max_workers=self.TASK_LIMIT)
|
||||
future = self._executor.submit(target, *args)
|
||||
future.add_done_callback(lambda f: GLib.idle_add(self._app.emit, "task-done", self))
|
||||
self._task = Task.new(self, Cancellable.new(), lambda s, t: GLib.idle_add(self._app.emit, "task-done", self))
|
||||
self._task.set_priority(GLib.PRIORITY_LOW)
|
||||
self._task.set_return_on_cancel(True)
|
||||
self._task.run_in_thread(lambda t, s, d, c: target(*args))
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
@@ -78,7 +79,10 @@ class BGTaskWidget(Gtk.Box):
|
||||
self.set_tooltip_text(value)
|
||||
|
||||
def cancel(self):
|
||||
self._executor.shutdown(wait=False)
|
||||
cancelable = self._task.get_cancellable()
|
||||
if cancelable:
|
||||
cancelable.cancel()
|
||||
|
||||
self._app.emit("task-canceled", None)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -27,43 +27,16 @@
|
||||
|
||||
|
||||
import re
|
||||
import selectors
|
||||
import socket
|
||||
from collections import deque
|
||||
from telnetlib import Telnet
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_task, run_idle, log
|
||||
from app.connections import ExtTelnet
|
||||
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
|
||||
|
||||
|
||||
class ExtTelnet(Telnet):
|
||||
|
||||
def __init__(self, output_callback, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._output_callback = output_callback
|
||||
|
||||
def interact(self):
|
||||
""" Interaction function, emulates a very dumb telnet client. """
|
||||
with selectors.DefaultSelector() as selector:
|
||||
selector.register(self, selectors.EVENT_READ)
|
||||
|
||||
while True:
|
||||
for key, events in selector.select():
|
||||
if key.fileobj is self:
|
||||
try:
|
||||
text = self.read_very_eager()
|
||||
except EOFError as e:
|
||||
msg = "\n*** Connection closed by remote host ***\n"
|
||||
self._output_callback(msg)
|
||||
log(msg)
|
||||
raise e
|
||||
else:
|
||||
if text:
|
||||
self._output_callback(text)
|
||||
|
||||
|
||||
class TelnetClient(Gtk.Box):
|
||||
""" Very simple telnet client. """
|
||||
_COLOR_PATTERN = re.compile("\x1b.*?m") # Color info
|
||||
@@ -156,11 +129,10 @@ class TelnetClient(Gtk.Box):
|
||||
self.do_command()
|
||||
return True
|
||||
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
key = KeyboardKey(event.hardware_keycode)
|
||||
if key is KeyboardKey.UNDEFINED:
|
||||
return False
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & MOD_MASK
|
||||
if ctrl and key is KeyboardKey.C:
|
||||
if self._tn and self._tn.sock:
|
||||
@@ -181,6 +153,7 @@ class TelnetClient(Gtk.Box):
|
||||
self._commands.append(cmd)
|
||||
self._buf.insert_at_cursor(cmd, -1)
|
||||
return True
|
||||
return False
|
||||
|
||||
def delete_last_command(self):
|
||||
end = self._buf.get_end_iter()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2024 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
|
||||
@@ -27,12 +27,12 @@ Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<requires lib="gtk+" version="3.22"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor. -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2024 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkAdjustment" id="begins_hour_adjustment">
|
||||
<property name="upper">23</property>
|
||||
@@ -307,8 +307,11 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkFrame" id="timer_dialog_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="width-request">325</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="label-xalign">0</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -31,9 +31,8 @@ from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from urllib.parse import quote
|
||||
|
||||
from app.settings import USE_HEADER_BAR
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .dialogs import get_builder, translate, show_dialog, DialogType
|
||||
from .dialogs import get_builder, translate, show_dialog, DialogType, BaseDialog
|
||||
from .uicommons import Gtk, Gdk, GLib, UI_RESOURCES_PATH, Page, Column, KeyboardKey, MOD_MASK
|
||||
from ..commons import run_idle, log
|
||||
from ..connections import HttpAPI
|
||||
@@ -55,9 +54,11 @@ class TimerTool(Gtk.Box):
|
||||
EVENT = 1
|
||||
CHANGE = 2
|
||||
|
||||
class TimerDialog(Gtk.Dialog):
|
||||
class TimerDialog(BaseDialog):
|
||||
def __init__(self, parent, action=None, timer_data=None, *args, **kwargs):
|
||||
super().__init__(use_header_bar=USE_HEADER_BAR, *args, **kwargs)
|
||||
super().__init__(parent=parent, title="Timer",
|
||||
buttons=(translate("Cancel"), Gtk.ResponseType.CANCEL,
|
||||
translate("Save"), Gtk.ResponseType.OK), *args, **kwargs)
|
||||
|
||||
self._action = action or TimerTool.TimerAction.ADD
|
||||
self._timer_data = timer_data or {}
|
||||
@@ -71,14 +72,6 @@ class TimerTool(Gtk.Box):
|
||||
"min_end_adjustment", "timer_begins_popover", "begins_hour_adjustment",
|
||||
"min_begins_adjustment"))
|
||||
|
||||
self.set_title(translate("Timer"))
|
||||
self.set_modal(True)
|
||||
self.set_skip_pager_hint(True)
|
||||
self.set_skip_taskbar_hint(True)
|
||||
self.set_transient_for(parent)
|
||||
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
|
||||
self.set_resizable(False)
|
||||
|
||||
self._timer_name_entry = builder.get_object("timer_name_entry")
|
||||
self._timer_desc_entry = builder.get_object("timer_desc_entry")
|
||||
self._timer_service_entry = builder.get_object("timer_service_entry")
|
||||
@@ -111,8 +104,7 @@ class TimerTool(Gtk.Box):
|
||||
self._timer_desc_entry.drag_dest_unset()
|
||||
self._timer_service_entry.drag_dest_unset()
|
||||
|
||||
self.add_buttons(translate("Cancel"), Gtk.ResponseType.CANCEL, translate("Save"), Gtk.ResponseType.OK)
|
||||
self.get_content_area().pack_start(builder.get_object("timer_dialog_frame"), True, True, 5)
|
||||
self.get_content_area().pack_start(builder.get_object("timer_dialog_frame"), True, True, 0)
|
||||
|
||||
if self._action is TimerTool.TimerAction.ADD:
|
||||
self.set_timer_for_add()
|
||||
@@ -363,9 +355,11 @@ class TimerTool(Gtk.Box):
|
||||
|
||||
if p_count == 1:
|
||||
service = self._app.current_services.get(model[paths][Column.FAV_ID], None)
|
||||
if service:
|
||||
if service and service.picon_id:
|
||||
self.add_timer({"e2servicename": service.service,
|
||||
"e2servicereference": service.picon_id.rstrip(".png").replace("_", ":")})
|
||||
else:
|
||||
self._app.show_error_message("Not allowed in this context!")
|
||||
elif p_count > 1:
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
else:
|
||||
@@ -466,11 +460,10 @@ class TimerTool(Gtk.Box):
|
||||
on_popup_menu(menu, event)
|
||||
|
||||
def on_timers_key_release(self, view, event):
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
key = KeyboardKey(event.hardware_keycode)
|
||||
if key is KeyboardKey.UNDEFINED:
|
||||
return
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & MOD_MASK
|
||||
|
||||
if key is KeyboardKey.DELETE:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -119,8 +119,10 @@ LOCKED_ICON = get_icon("changes-prevent-symbolic", 16, _IMAGE_MISSING)
|
||||
HIDE_ICON = get_icon("go-jump", 16, _IMAGE_MISSING)
|
||||
TV_ICON = get_icon("tv-symbolic", 16, _IMAGE_MISSING)
|
||||
IPTV_ICON = get_icon("emblem-shared", 16, _IMAGE_MISSING)
|
||||
LINK_ICON = get_icon("emblem-symbolic-link", 16, _IMAGE_MISSING)
|
||||
FOLDER_ICON = get_icon("folder-symbolic" if IS_DARWIN else "folder", 16, _IMAGE_MISSING)
|
||||
EPG_ICON = get_icon("gtk-index", 16, _IMAGE_MISSING)
|
||||
DEFAULT_ICON = get_icon("emblem-default", 16, _IMAGE_MISSING)
|
||||
DEFAULT_ICON = get_icon("emblem-default", 16, get_icon("emblem-default-symbolic", 16, _IMAGE_MISSING))
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
@@ -141,7 +143,7 @@ def get_yt_icon(icon_name, size=24):
|
||||
if n_theme.has_icon(icon_name):
|
||||
return n_theme.load_icon(icon_name, size, 0)
|
||||
|
||||
return default_theme.load_icon("emblem-important-symbolic", size, 0)
|
||||
return get_icon("emblem-important-symbolic", size, LINK_ICON)
|
||||
|
||||
|
||||
def show_notification(message, timeout=10000, urgency=1):
|
||||
@@ -192,6 +194,7 @@ class ViewTarget(Enum):
|
||||
FAV = 1
|
||||
SERVICES = 2
|
||||
IPTV = 3
|
||||
ALT = 4
|
||||
|
||||
|
||||
class BqGenType(Enum):
|
||||
@@ -287,14 +290,8 @@ class Column(IntEnum):
|
||||
|
||||
# *************** Keyboard keys *************** #
|
||||
|
||||
class BaseKeyboardKey(Enum):
|
||||
@classmethod
|
||||
def value_exist(cls, value):
|
||||
return value in (val.value for val in cls.__members__.values())
|
||||
|
||||
|
||||
if IS_LINUX:
|
||||
class KeyboardKey(BaseKeyboardKey):
|
||||
class KeyboardKey(IntEnum):
|
||||
""" The raw(hardware) codes [Linux] of the keyboard keys. """
|
||||
E = 26
|
||||
R = 27
|
||||
@@ -332,8 +329,14 @@ if IS_LINUX:
|
||||
PAGE_UP_KP = 81
|
||||
PAGE_DOWN_KP = 89
|
||||
|
||||
UNDEFINED = -1
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.UNDEFINED
|
||||
|
||||
elif IS_DARWIN:
|
||||
class KeyboardKey(BaseKeyboardKey):
|
||||
class KeyboardKey(IntEnum):
|
||||
""" The raw(hardware) codes [macOS] of the keyboard keys. """
|
||||
F = 3
|
||||
E = 14
|
||||
@@ -373,8 +376,14 @@ elif IS_DARWIN:
|
||||
PAGE_UP_KP = -1
|
||||
PAGE_DOWN_KP = -1
|
||||
|
||||
UNDEFINED = -1
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.UNDEFINED
|
||||
|
||||
else:
|
||||
class KeyboardKey(BaseKeyboardKey):
|
||||
class KeyboardKey(IntEnum):
|
||||
""" The raw(hardware) codes [Windows] of the keyboard keys. """
|
||||
E = 69
|
||||
R = 82
|
||||
@@ -412,6 +421,12 @@ else:
|
||||
PAGE_UP_KP = -1
|
||||
PAGE_DOWN_KP = -1
|
||||
|
||||
UNDEFINED = -1
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.UNDEFINED
|
||||
|
||||
# Keys for move in lists. KEY_KP_(NAME) for laptop!
|
||||
MOVE_KEYS = {KeyboardKey.UP, KeyboardKey.PAGE_UP,
|
||||
KeyboardKey.DOWN, KeyboardKey.PAGE_DOWN,
|
||||
@@ -419,5 +434,27 @@ MOVE_KEYS = {KeyboardKey.UP, KeyboardKey.PAGE_UP,
|
||||
KeyboardKey.HOME_KP, KeyboardKey.END_KP,
|
||||
KeyboardKey.PAGE_UP_KP, KeyboardKey.PAGE_DOWN_KP}
|
||||
|
||||
|
||||
class LoadingProgressBar(Gtk.ProgressBar):
|
||||
""" A custom class for a progress bar.
|
||||
|
||||
Used as an alternative to Gtk.Spinner to reduce CPU load.
|
||||
"""
|
||||
__gtype_name__ = "LoadingProgressBar"
|
||||
|
||||
def __init__(self, **properties):
|
||||
super().__init__(**properties)
|
||||
|
||||
self.connect("notify::visible", self.on_visible)
|
||||
|
||||
def on_visible(self, bar, param):
|
||||
if self.get_visible():
|
||||
GLib.timeout_add(500, self.update, priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def update(self):
|
||||
self.pulse()
|
||||
return self.get_visible()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2
|
||||
<!-- Generated with glade 3.40.0
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2025 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
|
||||
@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2025 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<!-- n-columns=2 n-rows=4 -->
|
||||
<object class="GtkGrid" id="cable_tr_box">
|
||||
@@ -471,7 +471,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkSpinButton" id="sat_position_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="width-chars">5</property>
|
||||
<property name="width-chars">6</property>
|
||||
<property name="secondary-icon-activatable">False</property>
|
||||
<property name="secondary-icon-sensitive">False</property>
|
||||
<property name="input-purpose">number</property>
|
||||
@@ -879,7 +879,7 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<!-- n-columns=2 n-rows=4 -->
|
||||
<!-- n-columns=2 n-rows=5 -->
|
||||
<object class="GtkGrid" id="tr_dialog_grid2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
@@ -911,7 +911,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="max-width-chars">12</property>
|
||||
<property name="primary-icon-name">document-edit-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="placeholder-text" translatable="yes">0 - 262142</property>
|
||||
<property name="placeholder-text">0 - 262142</property>
|
||||
<property name="input-purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
@@ -928,7 +928,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="max-width-chars">12</property>
|
||||
<property name="primary-icon-name">document-edit-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="placeholder-text" translatable="yes">0 - 255</property>
|
||||
<property name="placeholder-text">0 - 255</property>
|
||||
<property name="input-purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
@@ -945,7 +945,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="max-width-chars">12</property>
|
||||
<property name="primary-icon-name">document-edit-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="placeholder-text" translatable="yes">0 - 255</property>
|
||||
<property name="placeholder-text">0 - 255</property>
|
||||
<property name="input-purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
@@ -1095,6 +1095,58 @@ Author: Dmitriy Yefremov
|
||||
<property name="top-attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="tr_t2mi_pid_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">T2-MI PID</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label">:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="t2mi_pid_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="width-chars">5</property>
|
||||
<property name="max-width-chars">12</property>
|
||||
<property name="primary-icon-name">document-edit-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="placeholder-text">0 - 8191</property>
|
||||
<property name="input-purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2025 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
|
||||
@@ -207,6 +207,7 @@ class SatTransponderDialog(TransponderDialog):
|
||||
self._pls_code_entry = builder.get_object("pls_code_entry")
|
||||
self._is_id_entry = builder.get_object("is_id_entry")
|
||||
self._t2mi_plp_id_entry = builder.get_object("t2mi_plp_id_entry")
|
||||
self._t2mi_pid_entry = builder.get_object("t2mi_pid_entry")
|
||||
|
||||
self.set_style_provider(self._freq_entry)
|
||||
self.set_style_provider(self._rate_entry)
|
||||
@@ -230,6 +231,7 @@ class SatTransponderDialog(TransponderDialog):
|
||||
self._is_id_entry.set_text(transponder.is_id if transponder.is_id else "")
|
||||
self._pls_code_entry.set_text(transponder.pls_code if transponder.pls_code else "")
|
||||
self._t2mi_plp_id_entry.set_text(transponder.t2mi_plp_id if transponder.t2mi_plp_id else "")
|
||||
self._t2mi_pid_entry.set_text(transponder.t2mi_pid if transponder.t2mi_pid else "")
|
||||
|
||||
def to_transponder(self):
|
||||
return Transponder(frequency=self._freq_entry.get_text(),
|
||||
@@ -241,7 +243,8 @@ class SatTransponderDialog(TransponderDialog):
|
||||
pls_mode=get_key_by_value(PLS_MODE, self._pls_mode_box.get_active_id()),
|
||||
pls_code=self._pls_code_entry.get_text(),
|
||||
is_id=self._is_id_entry.get_text(),
|
||||
t2mi_plp_id=self._t2mi_plp_id_entry.get_text())
|
||||
t2mi_plp_id=self._t2mi_plp_id_entry.get_text(),
|
||||
t2mi_pid=self._t2mi_pid_entry.get_text())
|
||||
|
||||
def is_accept(self):
|
||||
tr = self.to_transponder()
|
||||
@@ -255,6 +258,8 @@ class SatTransponderDialog(TransponderDialog):
|
||||
return False
|
||||
elif self.digit_pattern.search(tr.t2mi_plp_id):
|
||||
return False
|
||||
elif self.digit_pattern.search(tr.t2mi_pid):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -401,8 +406,6 @@ class UpdateDialog:
|
||||
"on_satellite_changed": self.on_satellite_changed,
|
||||
"on_transponder_toggled": self.on_transponder_toggled,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_filter_toggled": self.on_filter_toggled,
|
||||
"on_find_toggled": self.on_find_toggled,
|
||||
"on_popup_menu": on_popup_menu,
|
||||
"on_select_all": self.on_select_all,
|
||||
"on_unselect_all": self.on_unselect_all,
|
||||
@@ -441,7 +444,7 @@ class UpdateDialog:
|
||||
self._left_action_box = builder.get_object("sat_update_left_action_box")
|
||||
self._right_action_box = builder.get_object("sat_update_right_action_box")
|
||||
# Filter
|
||||
self._filter_bar = builder.get_object("sat_update_filter_bar")
|
||||
self._filter_bar_box = builder.get_object("filter_bar_box")
|
||||
self._from_pos_button = builder.get_object("from_pos_button")
|
||||
self._to_pos_button = builder.get_object("to_pos_button")
|
||||
self._filter_from_combo_box = builder.get_object("filter_from_combo_box")
|
||||
@@ -449,18 +452,16 @@ class UpdateDialog:
|
||||
self._filter_model = builder.get_object("update_sat_list_model_filter")
|
||||
self._filter_model.set_visible_func(self.filter_function)
|
||||
self._filter_positions = (0, 0)
|
||||
self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible")
|
||||
# Log.
|
||||
self._log_frame = builder.get_object("log_frame")
|
||||
builder.get_object("log_info_bar").connect("response", lambda b, r: self._log_frame.set_visible(False))
|
||||
# Search.
|
||||
self._search_bar = builder.get_object("sat_update_search_bar")
|
||||
self._search_bar.bind_property("search-mode-enabled", self._search_bar, "visible")
|
||||
self._search_bar_box = builder.get_object("search_bar_box")
|
||||
search_provider = SearchProvider(self._sat_view,
|
||||
builder.get_object("sat_update_search_entry"),
|
||||
builder.get_object("sat_update_search_down_button"),
|
||||
builder.get_object("sat_update_search_up_button"))
|
||||
builder.get_object("sat_update_find_button").connect("toggled", search_provider.on_search_toggled)
|
||||
builder.get_object("search_button").connect("toggled", search_provider.on_search_toggled)
|
||||
# Satellite lists init on dialog start.
|
||||
self._sat_view.connect("realize", self.on_update_satellites_list)
|
||||
# Options.
|
||||
@@ -545,6 +546,8 @@ class UpdateDialog:
|
||||
@run_idle
|
||||
def append_satellites(self, sats):
|
||||
model = get_base_model(self._sat_view.get_model())
|
||||
if not model:
|
||||
return
|
||||
|
||||
for sat in sats:
|
||||
itr = model.append(sat)
|
||||
@@ -597,10 +600,10 @@ class UpdateDialog:
|
||||
self._sat_update_info_bar.set_visible(False)
|
||||
|
||||
def on_find_toggled(self, button: Gtk.ToggleToolButton):
|
||||
self._search_bar.set_search_mode(button.get_active())
|
||||
self._search_bar_box.set_visible(button.get_active())
|
||||
|
||||
def on_filter_toggled(self, button: Gtk.ToggleToolButton):
|
||||
self._filter_bar.set_search_mode(button.get_active())
|
||||
self._filter_bar_box.set_visible(button.get_active())
|
||||
|
||||
@run_idle
|
||||
def on_filter(self, item):
|
||||
@@ -684,8 +687,15 @@ class SatellitesUpdateDialog(UpdateDialog):
|
||||
box.pack_start(Gtk.Label(translate("Merge satellites by positions")), False, True, 0)
|
||||
box.pack_end(self._merge_sat_switch, False, True, 0)
|
||||
self._general_options_box.pack_start(box, True, True, 0)
|
||||
self._general_options_box.show_all()
|
||||
|
||||
self._split_band_switch = Gtk.Switch(active=self._dialog_settings.get("split_by_band", False))
|
||||
self._split_band_switch.connect("state-set", lambda b, s: self._dialog_settings.update({"split_by_band": s}))
|
||||
box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL)
|
||||
box.pack_start(Gtk.Label(translate("Split satellites by bands (C/KU)")), False, True, 0)
|
||||
box.pack_end(self._split_band_switch, False, True, 0)
|
||||
self._general_options_box.pack_start(box, True, True, 0)
|
||||
|
||||
self._general_options_box.show_all()
|
||||
self._skip_c_band_switch.get_parent().set_visible(False)
|
||||
|
||||
@run_idle
|
||||
@@ -756,6 +766,28 @@ class SatellitesUpdateDialog(UpdateDialog):
|
||||
else:
|
||||
sats = {s.name: s for s in sats} # key = name, v = satellite
|
||||
|
||||
# Post-processing if band separation is active.
|
||||
if self._split_band_switch.get_active():
|
||||
appender.send(f"Checking and splitting satellites by band...\n")
|
||||
to_remove = []
|
||||
new_sats = {}
|
||||
for name, sat in sats.items():
|
||||
# Checking for C/KU-transponders.
|
||||
c_tr = []
|
||||
ku_tr = []
|
||||
[c_tr.append(t) if int(t.frequency) < 10000000 else ku_tr.append(t) for t in sat.transponders]
|
||||
|
||||
if ku_tr and c_tr:
|
||||
c_sat = Satellite(f"{name} (C)", sat.flags, sat.position, c_tr)
|
||||
ku_sat = Satellite(f"{name} (KU)", sat.flags, sat.position, ku_tr)
|
||||
new_sats[c_sat.name] = c_sat
|
||||
new_sats[ku_sat.name] = ku_sat
|
||||
to_remove.append(name)
|
||||
|
||||
[sats.pop(n) for n in to_remove]
|
||||
sats.update(new_sats)
|
||||
appender.send("-" * _len + "\n")
|
||||
|
||||
for row in self._main_model:
|
||||
pos = row[0]
|
||||
if pos in sats:
|
||||
@@ -972,14 +1004,14 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
no_lb = "No Category"
|
||||
|
||||
if self._kos_bq_groups_switch.get_active():
|
||||
self.gen_bouquet_group(tv_services, tv_bouquets, lambda s: s[4] or no_lb)
|
||||
self.gen_bouquet_group(rd_services, radio_bouquets, lambda s: s[4] or no_lb, bq_type=BqType.RADIO.value)
|
||||
self.gen_bouquet_group(tv_services, tv_bouquets, lambda s: s[5] or no_lb)
|
||||
self.gen_bouquet_group(rd_services, radio_bouquets, lambda s: s[5] or no_lb, bq_type=BqType.RADIO.value)
|
||||
|
||||
if self._kos_bq_lang_switch.get_active():
|
||||
lb = "" if no_lb in {b.name for b in tv_bouquets} else "No Region"
|
||||
self.gen_bouquet_group(tv_services, tv_bouquets, lambda s: s[5] or lb)
|
||||
self.gen_bouquet_group(tv_services, tv_bouquets, lambda s: s[4] or lb)
|
||||
lb = "" if no_lb in {b.name for b in radio_bouquets} else "No Region"
|
||||
self.gen_bouquet_group(rd_services, radio_bouquets, lambda s: s[5] or lb, bq_type=BqType.RADIO.value)
|
||||
self.gen_bouquet_group(rd_services, radio_bouquets, lambda s: s[4] or lb, bq_type=BqType.RADIO.value)
|
||||
|
||||
return Bouquets("", BqType.TV.value, tv_bouquets), Bouquets("", BqType.RADIO.value, radio_bouquets)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2026 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
|
||||
@@ -239,12 +239,6 @@ class SatellitesTool(Gtk.Box):
|
||||
self._transponders_stack.set_visible_child_name(self._dvb_type)
|
||||
self._update_header_button.set_sensitive(self._dvb_type is self.DVB.SAT)
|
||||
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
self._app.on_info_bar_close()
|
||||
|
||||
else:
|
||||
self._app.show_info_message("EXPERIMENTAL!", Gtk.MessageType.WARNING)
|
||||
|
||||
def on_satellite_selection(self, view):
|
||||
model = self._sat_tr_view.get_model()
|
||||
model.clear()
|
||||
@@ -310,11 +304,10 @@ class SatellitesTool(Gtk.Box):
|
||||
|
||||
def on_key_press(self, view, event):
|
||||
""" Handling keystrokes. """
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
key = KeyboardKey(event.hardware_keycode)
|
||||
if key is KeyboardKey.UNDEFINED:
|
||||
return
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & MOD_MASK
|
||||
|
||||
if key is KeyboardKey.DELETE:
|
||||
@@ -329,12 +322,11 @@ class SatellitesTool(Gtk.Box):
|
||||
view.do_unselect_all(view)
|
||||
|
||||
def on_tr_key_press(self, view, event):
|
||||
""" Handling transponder view keystrokes. """
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
""" Handling transponder view keystrokes. """
|
||||
key = KeyboardKey(event.hardware_keycode)
|
||||
if key is KeyboardKey.UNDEFINED:
|
||||
return
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & MOD_MASK
|
||||
|
||||
if key is KeyboardKey.DELETE:
|
||||
@@ -580,14 +572,14 @@ class SatellitesTool(Gtk.Box):
|
||||
|
||||
def on_download(self, app, page):
|
||||
if page is Page.SATELLITE:
|
||||
self._app.on_download_data(DownloadType.SATELLITES)
|
||||
self._app.on_download_data(DownloadType.SATELLITES, files_filter=(f"{self._dvb_type}.xml",))
|
||||
|
||||
def on_upload(self, app, page):
|
||||
if page is Page.SATELLITE:
|
||||
self._app.upload_data(DownloadType.SATELLITES)
|
||||
self._app.upload_data(DownloadType.SATELLITES, files_filter=(f"{self._dvb_type}.xml",))
|
||||
|
||||
@run_idle
|
||||
def on_update(self, item):
|
||||
def on_update(self, item=None):
|
||||
SatellitesUpdateDialog(self._app.get_active_window(), self._settings, self._satellite_view.get_model()).show()
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2025 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
|
||||
@@ -128,6 +128,8 @@ Author: Dmitriy Yefremov
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name t2mi_plp_id -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name t2mi_pid -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<signal name="row-deleted" handler="on_sat_tr_model_changed" swapped="no"/>
|
||||
<signal name="row-inserted" handler="on_sat_tr_model_changed" swapped="no"/>
|
||||
@@ -424,7 +426,9 @@ Author: Dmitriy Yefremov
|
||||
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
|
||||
<signal name="realize" handler="on_terrestrial_view_realize" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection"/>
|
||||
<object class="GtkTreeSelection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ter_name_column">
|
||||
@@ -522,7 +526,9 @@ Author: Dmitriy Yefremov
|
||||
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
|
||||
<signal name="realize" handler="on_cable_view_realize" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection"/>
|
||||
<object class="GtkTreeSelection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="cable_name_column">
|
||||
@@ -947,7 +953,9 @@ Author: Dmitriy Yefremov
|
||||
<signal name="button-press-event" handler="on_tr_button_press" object="transponder_popup_menu" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_tr_key_press" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection"/>
|
||||
<object class="GtkTreeSelection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ter_freq_column">
|
||||
@@ -1188,7 +1196,9 @@ Author: Dmitriy Yefremov
|
||||
<signal name="button-press-event" handler="on_tr_button_press" object="transponder_popup_menu" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_tr_key_press" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection"/>
|
||||
<object class="GtkTreeSelection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="cable_freq_column">
|
||||
|
||||
@@ -41,8 +41,8 @@ Author: Dmitriy Yefremov
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
@@ -176,24 +176,6 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="sat_receive_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">network-receive-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="sat_update_cancel_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">window-close-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="sat_update_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">emblem-synchronizing-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="side_store">
|
||||
<columns>
|
||||
<!-- column-name side -->
|
||||
@@ -306,14 +288,20 @@ Author: Dmitriy Yefremov
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="sat_update_button">
|
||||
<property name="label" translatable="yes">Update</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Update</property>
|
||||
<property name="image">sat_update_image</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="on_update_satellites_list" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="update_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">emblem-synchronizing-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -323,13 +311,19 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_data_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Cancel</property>
|
||||
<property name="image">sat_update_cancel_image</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="on_cancel_receive" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="cancel_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">window-close-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="z" signal="clicked" modifiers="Primary"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -340,15 +334,21 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="receive_data_button">
|
||||
<property name="label" translatable="yes">Receive</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Receive</property>
|
||||
<property name="image">sat_receive_image</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="on_receive_data" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="sat_receive_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">network-receive-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -368,66 +368,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="sat_update_fs_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="sat_update_filter_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Filter</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="toggled" handler="on_filter_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="sat_update_filter_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-find-replace-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="f" signal="clicked" modifiers="GDK_SHIFT_MASK | Primary"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="sat_update_find_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Find</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="toggled" handler="on_find_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="sat_update_search_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-find-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="f" signal="clicked" modifiers="Primary"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="options_menu_button">
|
||||
<property name="visible">True</property>
|
||||
@@ -436,36 +376,12 @@ Author: Dmitriy Yefremov
|
||||
<property name="direction">none</property>
|
||||
<property name="popover">options_popover</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="options_button_box">
|
||||
<object class="GtkImage" id="options_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="options_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Options</property>
|
||||
<property name="icon-name">applications-system-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="options_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Options</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<property name="tooltip-text" translatable="yes">Options</property>
|
||||
<property name="icon-name">applications-system-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@@ -500,206 +416,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSearchBar" id="sat_update_search_bar">
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="search_bar_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-top">2</property>
|
||||
<property name="margin-bottom">2</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="sat_update_search_entry">
|
||||
<property name="width-request">200</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="primary-icon-name">edit-find-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="primary-icon-sensitive">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="sat_update_search_down_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="arrow1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="arrow-type">down</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="sat_update_search_up_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="arrow2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="arrow-type">up</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="group"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSearchBar" id="sat_update_filter_bar">
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<!-- n-columns=7 n-rows=1 -->
|
||||
<object class="GtkGrid" id="source_header_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-top">2</property>
|
||||
<property name="margin-bottom">2</property>
|
||||
<property name="column-spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="from_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">From:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="from_pos_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="input-purpose">number</property>
|
||||
<property name="adjustment">pos_adjustment</property>
|
||||
<property name="digits">1</property>
|
||||
<property name="numeric">True</property>
|
||||
<signal name="changed" handler="on_filter" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="filter_from_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="model">side_store</property>
|
||||
<property name="active">0</property>
|
||||
<signal name="changed" handler="on_filter" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="from_filter_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">2</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="to_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">To:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">3</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="to_pos_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="input-purpose">number</property>
|
||||
<property name="adjustment">pos_adjustment2</property>
|
||||
<property name="digits">1</property>
|
||||
<property name="numeric">True</property>
|
||||
<signal name="changed" handler="on_filter" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">4</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="filter_to_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="model">side_store</property>
|
||||
<property name="active">0</property>
|
||||
<signal name="changed" handler="on_filter" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="filter_to_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">5</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="filter_apply_button">
|
||||
<property name="label">gtk-apply</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="use-stock">True</property>
|
||||
<signal name="clicked" handler="on_filter" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">6</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkPaned" id="sat_update_main_paned">
|
||||
<property name="visible">True</property>
|
||||
@@ -727,10 +443,281 @@ Author: Dmitriy Yefremov
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="sat_header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="filter_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Filter</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="sat_update_filter_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-find-replace-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="f" signal="clicked" modifiers="GDK_SHIFT_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="search_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Find</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="sat_update_search_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-find-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="f" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="search_bar_box">
|
||||
<property name="visible" bind-source="search_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="sat_update_search_entry">
|
||||
<property name="width-request">200</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="primary-icon-name">edit-find-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="primary-icon-sensitive">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="sat_update_search_down_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="arrow1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="arrow-type">down</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="sat_update_search_up_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="arrow2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="arrow-type">up</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="group"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="filter_bar_box">
|
||||
<property name="visible" bind-source="filter_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<child type="center">
|
||||
<!-- n-columns=7 n-rows=1 -->
|
||||
<object class="GtkGrid" id="filter_bar_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="column-spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="from_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">From:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="from_pos_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="text" translatable="yes">0,0</property>
|
||||
<property name="input-purpose">number</property>
|
||||
<property name="adjustment">pos_adjustment</property>
|
||||
<property name="digits">1</property>
|
||||
<property name="numeric">True</property>
|
||||
<signal name="changed" handler="on_filter" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="filter_from_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="model">side_store</property>
|
||||
<property name="active">0</property>
|
||||
<signal name="changed" handler="on_filter" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="from_filter_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">2</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="to_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">To:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">3</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="to_pos_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="text" translatable="yes">0,0</property>
|
||||
<property name="input-purpose">number</property>
|
||||
<property name="adjustment">pos_adjustment2</property>
|
||||
<property name="digits">1</property>
|
||||
<property name="numeric">True</property>
|
||||
<signal name="changed" handler="on_filter" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">4</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="filter_to_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="model">side_store</property>
|
||||
<property name="active">0</property>
|
||||
<signal name="changed" handler="on_filter" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="filter_to_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">5</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="filter_apply_button">
|
||||
<property name="label">gtk-apply</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="use-stock">True</property>
|
||||
<signal name="clicked" handler="on_filter" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">6</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="sat_update_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="sat_update_tree_view">
|
||||
@@ -831,7 +818,7 @@ Author: Dmitriy Yefremov
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
@@ -870,7 +857,7 @@ Author: Dmitriy Yefremov
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
@@ -894,7 +881,7 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
<property name="shrink">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
@@ -1044,7 +1031,7 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
<property name="shrink">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
@@ -1235,7 +1222,7 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
<property name="shrink">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
@@ -5,11 +5,17 @@ The best way to run this program from source is using of [MSYS2](https://www.msy
|
||||

|
||||
3. Run first `pacman -Suy` After that, you may need to restart the terminal and re-run the update command.
|
||||
4. Install minimal required packages:
|
||||
`pacman -S mingw-w64-x86_64-gtk3 mingw-w64-x86_64-python3 mingw-w64-x86_64-python3-gobject mingw-w64-x86_64-python3-pip mingw-w64-x86_64-python3-requests`
|
||||
Optional: `pacman -S mingw-w64-x86_64-python3-pillow`
|
||||
`pacman -S mingw-w64-x86_64-gtk3 mingw-w64-x86_64-python3 mingw-w64-x86_64-python3-gobject mingw-w64-x86_64-python-requests`
|
||||
Optional: `pacman -S mingw-w64-x86_64-python-pillow mingw-w64-x86_64-python-chardet`
|
||||
To support streams playback, install the following packages (the list may not be complete):
|
||||
For [MPV](https://mpv.io/) `pacman -S mingw-w64-x86_64-mpv`,
|
||||
For [GStreamer](https://gstreamer.freedesktop.org/) `pacman -S mingw-w64-x86_64-gst-libav mingw-w64-x86_64-gst-plugins-bad mingw-w64-x86_64-gst-plugins-base mingw-w64-x86_64-gst-plugins-good mingw-w64-x86_64-gstreamer`
|
||||
* For [GStreamer](https://gstreamer.freedesktop.org/) `pacman -S mingw-w64-x86_64-gst-libav mingw-w64-x86_64-gst-plugins-bad mingw-w64-x86_64-gst-plugins-base mingw-w64-x86_64-gst-plugins-good mingw-w64-x86_64-gstreamer`
|
||||
* For [MPV](https://mpv.io/) `pacman -S mingw-w64-x86_64-mpv`,
|
||||
To reduce installation size or try the latest changes, we can install the *libmpv* [build](https://github.com/shinchiro/mpv-winbuild-cmake/releases) (**mpv-dev**-x86_64-v3-*.7z) by [shinchiro](https://github.com/shinchiro).
|
||||
* Download and extract 7z archive.
|
||||
* Copy libmpv-2.dll to *C:\msys64\mingw64\bin*
|
||||
* libmpv.dll.a to *C:\msys64\mingw64\lib*
|
||||
and folder *include\mpv to *C:\msys64\mingw64\include* path.
|
||||
|
||||
5. Download and unzip the archive with sources from preferred branch (e.g. [master](https://github.com/DYefremov/DemonEditor/archive/refs/heads/master.zip)) in to folder where MSYS2 is installed. E.g: `c:\msys64\home\username\`
|
||||
6. Run mingw64 shell. Go to the folder where the program was unpacked. E.g: `cd DemonEditor/`
|
||||
And run: `./start.py`
|
||||
@@ -17,7 +23,7 @@ And run: `./start.py`
|
||||
## Building a package
|
||||
To build a standalone package, we can use [PyInstaller](https://pyinstaller.readthedocs.io/en/stable/).
|
||||
1. Launch mingw64 shell.
|
||||
2. Install PyInstaller via pip: `pip3 install pyinstaller`
|
||||
2. Install PyInstaller: `pacman -S mingw-w64-x86_64-pyinstaller`
|
||||
3. Go to the folder where the program was unpacked. E.g: `c:\msys64\home\username\DemonEditor\`
|
||||
4. Сopy and replace the files from the /build/win/ folder to the root .
|
||||
5. Go to the folder with the program in the running terminal: `cd DemonEditor/`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
VER="3.9.0_Beta"
|
||||
VER="3.14.4_Beta"
|
||||
B_PATH="dist/DemonEditor"
|
||||
DEB_PATH="$B_PATH/usr/share/demoneditor"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package: demon-editor
|
||||
Version: 3.9.0-Beta
|
||||
Version: 3.14.4-Beta
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
@@ -10,7 +10,8 @@ Depends: python3 (>= 3.6),
|
||||
python3-gi-cairo,
|
||||
gir1.2-notify-0.7,
|
||||
p7zip-full
|
||||
Recommends: libmpv1,
|
||||
Recommends: ffmpeg,
|
||||
libmpv1,
|
||||
python3-chardet,
|
||||
libgtksourceview (>= 3.0)
|
||||
Maintainer: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
|
||||
|
||||
@@ -5,7 +5,7 @@ Source: https://github.com/DYefremov/DemonEditor
|
||||
Files: *
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2024 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2026 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
|
||||
|
||||
@@ -2,14 +2,27 @@
|
||||
Version=1.0
|
||||
Name=DemonEditor
|
||||
GenericName=Enigma2 bouquets editor
|
||||
GenericName[it]=Editor di bouquet per Enigma2
|
||||
GenericName[be]=Рэдактар букетаў Enigma2
|
||||
GenericName[de]=Enigma2 Bouquet-Editor
|
||||
GenericName[es]=Editor de ramos de Enigma2
|
||||
GenericName[it]=Editor di bouquet Enigma2
|
||||
GenericName[nl]=Enigma2 boeket editor
|
||||
GenericName[pl]=Edytor bukietów Enigma2
|
||||
GenericName[pt]=Editor de buquês Enigma2
|
||||
GenericName[ru]=Редактор букетов Enigma2
|
||||
GenericName[tr]=Enigma2 buket düzenleyici
|
||||
GenericName[zh_CN]=Enigma2频道编辑器
|
||||
Comment=Channel and satellite list editor for Enigma2
|
||||
Comment[be]=Рэдактар спісу каналаў і супутнікаў для Enigma2
|
||||
Comment[de]=Kanal- und Satellitenlisten-Editor für Enigma2
|
||||
Comment[es]=Editor de lista de canales y satélites para Enigma2
|
||||
Comment[it]=Editor di elenchi di canali e satelliti per Enigma2
|
||||
Comment[nl]=Kanaal- en satellietlijsteditor voor Enigma2
|
||||
Comment[pl]=Edytor list kanałów i satelitów dla Enigma2
|
||||
Comment[pt]=Editor de lista de canais e satélites para Enigma2
|
||||
Comment[ru]=Редактор списка каналов и спутников для Enigma2
|
||||
Comment[be]=Рэдактар спіса каналаў і спадарожнікаў для Enigma2
|
||||
Comment[de]=Programm- und Satellitenlisten-Editor für Enigma2
|
||||
Comment[it]=Editor di liste canali e satelliti per Enigma2
|
||||
Comment[tr]=Enigma2 için kanal ve uydu listesi editörü
|
||||
Comment[es]=Editor de listas de canales y satélites para Enigma2
|
||||
Comment[tr]=Enigma2 için Kanal ve uydu listesi düzenleyici
|
||||
Comment[zh_CN]=Enigma2频道和卫星列表编辑器
|
||||
Icon=demon-editor
|
||||
Exec=/usr/bin/demon-editor
|
||||
Terminal=false
|
||||
|
||||
@@ -32,13 +32,13 @@ a = Analysis([EXE_NAME],
|
||||
pathex=PATH_EXE,
|
||||
binaries=None,
|
||||
datas=ui_files,
|
||||
hiddenimports=['fileinput', 'uuid', 'asyncio'],
|
||||
hiddenimports=['fileinput', 'uuid', 'asyncio', 'getpass'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
hooksconfig={
|
||||
"gi": {
|
||||
"languages": ["en", "be", "es", "it", "nl",
|
||||
"pl", "pt", "ru", "tr", "zh_CN"],
|
||||
"languages": ["en", "be", "es", "it", "nl", "pl",
|
||||
"pt", "ru", "sk", "tr", "zh_CN"],
|
||||
"module-versions": {
|
||||
"Gtk": "3.0"
|
||||
},
|
||||
@@ -81,8 +81,8 @@ app = BUNDLE(coll,
|
||||
'CFBundleGetInfoString': "Enigma2 channel and satellite editor",
|
||||
'LSApplicationCategoryType': 'public.app-category.utilities',
|
||||
'LSMinimumSystemVersion': '10.13',
|
||||
'CFBundleShortVersionString': f"3.9.0.{BUILD_DATE} Beta",
|
||||
'NSHumanReadableCopyright': u"Copyright © 2024, Dmitriy Yefremov",
|
||||
'CFBundleShortVersionString': f"3.14.4.{BUILD_DATE} Beta",
|
||||
'NSHumanReadableCopyright': u"Copyright © 2018-2026, Dmitriy Yefremov",
|
||||
'NSRequiresAquaSystemAppearance': 'false',
|
||||
'NSHighResolutionCapable': 'true'
|
||||
})
|
||||
|
||||
@@ -7,8 +7,7 @@ PATH_EXE = [os.path.join(DIR_PATH, EXE_NAME)]
|
||||
block_cipher = None
|
||||
|
||||
|
||||
excludes = ['app.tools.mpv',
|
||||
'gi.repository.Gst',
|
||||
excludes = ['gi.repository.Gst',
|
||||
'gi.repository.GstBase',
|
||||
'gi.repository.GstVideo',
|
||||
'youtube_dl',
|
||||
@@ -30,13 +29,13 @@ a = Analysis([EXE_NAME],
|
||||
pathex=PATH_EXE,
|
||||
binaries=[],
|
||||
datas=ui_files,
|
||||
hiddenimports=['fileinput', 'uuid', 'ctypes.wintypes', 'asyncio'],
|
||||
hiddenimports=['fileinput', 'uuid', 'ctypes.wintypes', 'asyncio', 'getpass'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
hooksconfig={
|
||||
"gi": {
|
||||
"languages": ["en", "be", "es", "it", "nl",
|
||||
"pl", "pt", "ru", "tr", "zh_CN"],
|
||||
"languages": ["en", "be", "es", "it", "nl", "pl",
|
||||
"pt", "ru", "sk", "tr", "zh_CN"],
|
||||
"module-versions": {
|
||||
"Gtk": "3.0",
|
||||
"GtkSource": "3",
|
||||
@@ -48,9 +47,19 @@ a = Analysis([EXE_NAME],
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
|
||||
|
||||
splash = Splash('logo.png',
|
||||
binaries=a.binaries,
|
||||
datas=a.datas)
|
||||
|
||||
|
||||
exe = EXE(pyz,
|
||||
splash,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
@@ -59,13 +68,16 @@ exe = EXE(pyz,
|
||||
bootloader_ignore_signals=False,
|
||||
contents_directory='.',
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx=False,
|
||||
console=False,
|
||||
icon='icon.ico')
|
||||
|
||||
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
splash.binaries,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
|
||||
26
build/win/hide_splash.patch
Normal file
26
build/win/hide_splash.patch
Normal file
@@ -0,0 +1,26 @@
|
||||
Subject: [PATCH] hide_splash
|
||||
---
|
||||
Index: app/ui/main.py
|
||||
IDEA additional info:
|
||||
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||
<+>UTF-8
|
||||
===================================================================
|
||||
diff --git a/app/ui/main.py b/app/ui/main.py
|
||||
--- a/app/ui/main.py (revision 0fc0ef1d3e80fc84f4da81e1117db63a1f1d3467)
|
||||
+++ b/app/ui/main.py (date 1771419933854)
|
||||
@@ -651,6 +651,15 @@
|
||||
gen = self.init_http_api()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
+ if hasattr(sys, "_MEIPASS"):
|
||||
+ import pyi_splash
|
||||
+
|
||||
+ if pyi_splash.is_alive():
|
||||
+ pyi_splash.close()
|
||||
+
|
||||
+ if self._main_window.is_suspended():
|
||||
+ self._main_window.present()
|
||||
+
|
||||
def do_shutdown(self):
|
||||
""" Performs shutdown tasks """
|
||||
if self._settings.load_last_config:
|
||||
BIN
build/win/logo.png
Executable file
BIN
build/win/logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
@@ -1,14 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
from multiprocessing import freeze_support
|
||||
if hasattr(sys, "_MEIPASS"):
|
||||
import os
|
||||
import ssl
|
||||
from multiprocessing import freeze_support
|
||||
|
||||
os.environ["PYTHONUTF8"] = "1"
|
||||
freeze_support()
|
||||
# TODO There needs to be a more "correct" way.
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
from app.ui.main import start_app
|
||||
|
||||
os.environ["PYTHONUTF8"] = "1"
|
||||
# TODO There needs to be a more "correct" way.
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
freeze_support()
|
||||
start_app()
|
||||
|
||||
@@ -10,6 +10,7 @@ GenericName[nl]=Enigma2 boeket editor
|
||||
GenericName[pl]=Edytor bukietów Enigma2
|
||||
GenericName[pt]=Editor de buquês Enigma2
|
||||
GenericName[ru]=Редактор букетов Enigma2
|
||||
GenericName[sk]=Enigma2 editor balíčkov
|
||||
GenericName[tr]=Enigma2 buket düzenleyici
|
||||
GenericName[zh_CN]=Enigma2频道编辑器
|
||||
Comment=Channel and satellite list editor for Enigma2
|
||||
@@ -21,6 +22,7 @@ Comment[nl]=Kanaal- en satellietlijsteditor voor Enigma2
|
||||
Comment[pl]=Edytor list kanałów i satelitów dla Enigma2
|
||||
Comment[pt]=Editor de lista de canais e satélites para Enigma2
|
||||
Comment[ru]=Редактор списка каналов и спутников для Enigma2
|
||||
Comment[sk]=Editor zoznamu kanálov a satelitov pre Enigma2
|
||||
Comment[tr]=Enigma2 için Kanal ve uydu listesi düzenleyici
|
||||
Comment[zh_CN]=Enigma2频道和卫星列表编辑器
|
||||
Icon=demon-editor
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (C) 2018-2026 Dmitriy Yefremov
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
#
|
||||
@@ -11,7 +11,7 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "translator-credits"
|
||||
msgstr ""
|
||||
msgstr "Дзмітрый Яфрэмаў"
|
||||
|
||||
# Main
|
||||
msgid "Service"
|
||||
@@ -258,8 +258,14 @@ msgid "Extra:"
|
||||
msgstr "Дадаткова:"
|
||||
|
||||
# Filter bar
|
||||
msgid "Only free"
|
||||
msgstr "Толькі адкрытыя"
|
||||
msgid "Access"
|
||||
msgstr "Доступ"
|
||||
|
||||
msgid "Free (FTA)"
|
||||
msgstr "Адкрытыя (FTA)"
|
||||
|
||||
msgid "Coded"
|
||||
msgstr "Закадаваныя"
|
||||
|
||||
msgid "All positions"
|
||||
msgstr "Усе пазіцыі"
|
||||
@@ -332,8 +338,8 @@ msgstr "Шлях да піконаў фармату Enigma2:"
|
||||
msgid "Specify the correct position value for the provider!"
|
||||
msgstr "Пакажыце правільнае значэнне пазіцыі для правайдара!"
|
||||
|
||||
msgid "Converter between name formats"
|
||||
msgstr "Канвертар фармату імёнаў"
|
||||
msgid "Converter between formats"
|
||||
msgstr "Канвертар фарматаў"
|
||||
|
||||
msgid "Receive picons for providers"
|
||||
msgstr "Атрыманне піконаў для правайдараў"
|
||||
@@ -1542,3 +1548,69 @@ msgstr "Падзел па групах"
|
||||
|
||||
msgid "Create sub-bouquets"
|
||||
msgstr "Стварыць падбукеты"
|
||||
|
||||
msgid "Add image"
|
||||
msgstr "Дадаць выяву"
|
||||
|
||||
msgid "TV Format"
|
||||
msgstr "ТБ-фармат"
|
||||
|
||||
msgid "Use with Streamrelay"
|
||||
msgstr "Скарыстаць Streamrelay"
|
||||
|
||||
msgid "Remove use with Streamrelay"
|
||||
msgstr "Не выкарыстоўваць Streamrelay"
|
||||
|
||||
msgid "Enable additional name cache for EPG"
|
||||
msgstr "Уключыць дадатковы кэш імёнаў для EPG"
|
||||
|
||||
msgid "Enables additional cache to display EPG for some IPTV channels imported from *.m3u."
|
||||
msgstr "Улучае дадатковы кэш для адлюстравання EPG некаторых каналаў IPTV імпартаваных з *.m3u."
|
||||
|
||||
msgid "Enable deep name comparison"
|
||||
msgstr "Уключыць глыбокае параўнанне імёнаў"
|
||||
|
||||
msgid "Enables deeper name matching. Possible inaccuracies!"
|
||||
msgstr "Дазваляе глыбейшае параўнанне імёнаў. Магчымы недакладнасці!"
|
||||
|
||||
msgid "Convert for selected bouquets"
|
||||
msgstr "Канвертаваць для абраных букетаў"
|
||||
|
||||
msgid "There were errors [%s] during bouquets loading!"
|
||||
msgstr "Пры загрузцы букетаў узніклі памылкі [%s]!"
|
||||
|
||||
msgid "Check the log for more info."
|
||||
msgstr "Глядзіце логі для пашыранай інфармацыі."
|
||||
|
||||
msgid "Satellite channel"
|
||||
msgstr "Спадарожнікавы канал"
|
||||
|
||||
msgid "Terrestrial channel"
|
||||
msgstr "Эфірны канал"
|
||||
|
||||
msgid "Cable channel"
|
||||
msgstr "Кабельны канал"
|
||||
|
||||
msgid "Save current changes"
|
||||
msgstr "Захаваць бягучыя змены"
|
||||
|
||||
msgid "Create a new service"
|
||||
msgstr "Стварыць новы сэрвіс"
|
||||
|
||||
msgid "Extension support is disabled!"
|
||||
msgstr "Падтрымка пашырэнняў адключана!"
|
||||
|
||||
msgid "Do you want to enable it?"
|
||||
msgstr "Жадаеце ўключыць?"
|
||||
|
||||
msgid "Playback of IPTV streams only!"
|
||||
msgstr "Прайграванне толькі IPTV-патокаў!"
|
||||
|
||||
msgid "There are running background tasks!"
|
||||
msgstr "Ідзе выкананне фонавых задач!"
|
||||
|
||||
msgid "Check if FFmpeg is installed!"
|
||||
msgstr "Праверце, ці ўсталяваны FFmpeg!"
|
||||
|
||||
msgid "It can cause some problems."
|
||||
msgstr "Гэта можа выклікаць некаторыя праблемы."
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Copyright (C) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (C) 2018-2026 Dmitriy Yefremov
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
# Charly, 2019.
|
||||
# Dmitriy Yefremov, 2020-2024.
|
||||
# Dmitriy Yefremov, 2020-2026.
|
||||
# Thomas Schmidt, 2021.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
@@ -260,8 +260,14 @@ msgid "Extra:"
|
||||
msgstr "Extra:"
|
||||
|
||||
# Filter bar
|
||||
msgid "Only free"
|
||||
msgstr "Nur freie"
|
||||
msgid "Access"
|
||||
msgstr "Zugriff"
|
||||
|
||||
msgid "Free (FTA)"
|
||||
msgstr "Freie (FTA)"
|
||||
|
||||
msgid "Coded"
|
||||
msgstr "Codiert"
|
||||
|
||||
msgid "All positions"
|
||||
msgstr "Alle Positionen"
|
||||
@@ -334,8 +340,8 @@ msgstr "Pfad zu Enigma2-Picons:"
|
||||
msgid "Specify the correct position value for the provider!"
|
||||
msgstr "Geben Sie den richtigen Positionswert für den Provider an!"
|
||||
|
||||
msgid "Converter between name formats"
|
||||
msgstr "Konverter zwischen Namensformaten"
|
||||
msgid "Converter between formats"
|
||||
msgstr "Konverter zwischen Formaten"
|
||||
|
||||
msgid "Receive picons for providers"
|
||||
msgstr "Picons für Provider erhalten"
|
||||
@@ -1556,3 +1562,69 @@ msgstr "Aufteilung nach Gruppen"
|
||||
|
||||
msgid "Create sub-bouquets"
|
||||
msgstr "Sub-Bouquets erstellen"
|
||||
|
||||
msgid "Add image"
|
||||
msgstr "Bild hinzufügen"
|
||||
|
||||
msgid "TV Format"
|
||||
msgstr "TV-Format"
|
||||
|
||||
msgid "Use with Streamrelay"
|
||||
msgstr "Benutzung mit Streamrelay"
|
||||
|
||||
msgid "Remove use with Streamrelay"
|
||||
msgstr "Streamrelay nutzung löschen"
|
||||
|
||||
msgid "Enable additional name cache for EPG"
|
||||
msgstr "Zusätzlichen Namenscache für EPG aktivieren"
|
||||
|
||||
msgid "Enables additional cache to display EPG for some IPTV channels imported from *.m3u."
|
||||
msgstr "Aktiviert zusätzlichen Cache zur Anzeige des EPG für einige IPTV-Kanäle, die aus *.m3u importiert wurden."
|
||||
|
||||
msgid "Enable deep name comparison"
|
||||
msgstr "Tiefer Namensvergleich aktivieren"
|
||||
|
||||
msgid "Enables deeper name matching. Possible inaccuracies!"
|
||||
msgstr "Aktiviert tiefere Namensübereinstimmung. Mögliche Ungenauigkeiten!"
|
||||
|
||||
msgid "Convert for selected bouquets"
|
||||
msgstr "Konvertieren für ausgewählte Bouquets"
|
||||
|
||||
msgid "There were errors [%s] during bouquets loading!"
|
||||
msgstr "Beim Laden der Bouquets sind Fehler [%s] aufgetreten!"
|
||||
|
||||
msgid "Check the log for more info."
|
||||
msgstr "Weitere Informationen finden Sie im Log."
|
||||
|
||||
msgid "Satellite channel"
|
||||
msgstr "Satellitenkanal"
|
||||
|
||||
msgid "Terrestrial channel"
|
||||
msgstr "Terrestrischer Kanal"
|
||||
|
||||
msgid "Cable channel"
|
||||
msgstr "Kabelkanal"
|
||||
|
||||
msgid "Save current changes"
|
||||
msgstr "Aktuelle Änderungen speichern"
|
||||
|
||||
msgid "Create a new service"
|
||||
msgstr "Erstellen eines neuen Service"
|
||||
|
||||
msgid "Extension support is disabled!"
|
||||
msgstr "Die Unterstützung für Erweiterungen ist deaktiviert!"
|
||||
|
||||
msgid "Do you want to enable it?"
|
||||
msgstr "Möchtest du aktivieren?"
|
||||
|
||||
msgid "Playback of IPTV streams only!"
|
||||
msgstr "Wiedergabe nur IPTV-Streams!"
|
||||
|
||||
msgid "There are running background tasks!"
|
||||
msgstr "Es laufen Hintergrundprozesse!"
|
||||
|
||||
msgid "Check if FFmpeg is installed!"
|
||||
msgstr "Prüfe ob FFmpeg installiert ist!"
|
||||
|
||||
msgid "It can cause some problems."
|
||||
msgstr "Das kann zu einigen Problemen führen."
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Copyright (C) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (C) 2018-2026 Dmitriy Yefremov
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022, 2023, 2024 Massimo Pissarello <mapi68@gmail.com>
|
||||
# SPDX-FileCopyrightText: 2022, 2023, 2024, 2025, 2026 Massimo Pissarello <mapi68@gmail.com>
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"PO-Revision-Date: 2024-02-20 21:33+0100\n"
|
||||
"PO-Revision-Date: 2026-01-30 17:27+0100\n"
|
||||
"Last-Translator: Massimo Pissarello <mapi68@gmail.com>\n"
|
||||
"Language-Team: Italian <>\n"
|
||||
"Language: it\n"
|
||||
@@ -13,12 +13,10 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Lokalize 23.08.4\n"
|
||||
"X-Generator: Lokalize 25.12.1\n"
|
||||
|
||||
msgid "translator-credits"
|
||||
msgstr ""
|
||||
"Nicola Fanghella\n"
|
||||
"Massimo Pissarello"
|
||||
msgstr "Massimo Pissarello\nNicola Fanghella"
|
||||
|
||||
# Main
|
||||
msgid "Service"
|
||||
@@ -265,8 +263,14 @@ msgid "Extra:"
|
||||
msgstr "Extra:"
|
||||
|
||||
# Filter bar
|
||||
msgid "Only free"
|
||||
msgstr "Solo gratis"
|
||||
msgid "Access"
|
||||
msgstr "Accesso"
|
||||
|
||||
msgid "Free (FTA)"
|
||||
msgstr "Libero (FTA)"
|
||||
|
||||
msgid "Coded"
|
||||
msgstr "Codificato"
|
||||
|
||||
msgid "All positions"
|
||||
msgstr "Tutte le posizioni"
|
||||
@@ -728,7 +732,7 @@ msgid "XML file"
|
||||
msgstr "File XML"
|
||||
|
||||
msgid "Use web source"
|
||||
msgstr "Usa fonte web"
|
||||
msgstr "Usa sorgente web"
|
||||
|
||||
msgid "Url to *.xml.gz file:"
|
||||
msgstr "Da URL a file *.xml.gz:"
|
||||
@@ -824,8 +828,8 @@ msgstr "Abilita barra di riproduzione diretta"
|
||||
|
||||
msgid "Enables direct sending and playback of media links on the receiver"
|
||||
msgstr ""
|
||||
"Abilita l'invio e la riproduzione diretta di collegamenti multimediali sul"
|
||||
" ricevitore"
|
||||
"Abilita invio e la riproduzione diretta di collegamenti multimediali sul ricev"
|
||||
"itore"
|
||||
|
||||
msgid "Watch the channel in the program"
|
||||
msgstr "Guarda canale nel programma"
|
||||
@@ -1312,7 +1316,7 @@ msgid "Add picons"
|
||||
msgstr "Aggiungi picon"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "Registri"
|
||||
msgstr "Log"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Titolo"
|
||||
@@ -1324,7 +1328,7 @@ msgid "Length"
|
||||
msgstr "Durata"
|
||||
|
||||
msgid "Additional source"
|
||||
msgstr "Fonte aggiuntiva"
|
||||
msgstr "Sorgente aggiuntiva"
|
||||
|
||||
msgid "Automatically set the name selected in the favorites list."
|
||||
msgstr "Imposta automaticamente il nome selezionato nell'elenco dei preferiti."
|
||||
@@ -1412,8 +1416,8 @@ msgstr "Riproduci dall'elenco principale"
|
||||
|
||||
msgid "Enables URL parsing using yt-dlp to get direct links to media."
|
||||
msgstr ""
|
||||
"Abilita l'analisi degli URL utilizzando yt-dlp per ottenere collegamenti"
|
||||
" diretti ai contenuti multimediali."
|
||||
"Abilita analisi degli URL utilizzando yt-dlp per ottenere collegamenti diretti"
|
||||
" ai contenuti multimediali."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "Permessi..."
|
||||
@@ -1466,8 +1470,8 @@ msgid ""
|
||||
" selected.\n"
|
||||
" Recommended only if you have external storage."
|
||||
msgstr ""
|
||||
"Abilita il caricamento come archivio compresso se viene selezionato un numero"
|
||||
" elevato di picon (> 1000). Consigliato solo se si dispone di memoria esterna."
|
||||
"Abilita caricamento come archivio compresso se viene selezionato un numero ele"
|
||||
"vato di picon (> 1000). Consigliato solo se si dispone di memoria esterna."
|
||||
|
||||
msgid "Clear \"New\" flag"
|
||||
msgstr "Rimuovi il flag \"Nuovo\""
|
||||
@@ -1509,11 +1513,10 @@ msgid "Removed"
|
||||
msgstr "Rimosso"
|
||||
|
||||
msgid "Enables overwriting existing main list services."
|
||||
msgstr ""
|
||||
"Abilita la sovrascrittura dei servizi esistenti dell'elenco principale."
|
||||
msgstr "Abilita sovrascrittura dei servizi esistenti dell'elenco principale."
|
||||
|
||||
msgid "Enables skipping services import from lamedb."
|
||||
msgstr "Abilita il salto dell'importazione dei servizi da lamedb."
|
||||
msgstr "Abilita salto dell'importazione dei servizi da lamedb."
|
||||
|
||||
msgid "Bouquets data only"
|
||||
msgstr "Bouquet solo dati"
|
||||
@@ -1585,13 +1588,13 @@ msgid "Current EPG cache contents."
|
||||
msgstr "Contenuto attuale cache EPG."
|
||||
|
||||
msgid "Source error!"
|
||||
msgstr "Errore fonte!"
|
||||
msgstr "Errore sorgente!"
|
||||
|
||||
msgid "The EPG source for the favorites list is not set!"
|
||||
msgstr "La fonte EPG per l'elenco dei preferiti non è impostata!"
|
||||
msgstr "La sorgente EPG per l'elenco dei preferiti non è impostata!"
|
||||
|
||||
msgid "Add to EPG sources list"
|
||||
msgstr "Aggiungi all'elenco delle fonti EPG"
|
||||
msgstr "Aggiungi all'elenco delle sorgenti EPG"
|
||||
|
||||
msgid "Current bouquet"
|
||||
msgstr "Bouquet attuale"
|
||||
@@ -1604,3 +1607,62 @@ msgstr "Dividi per gruppi"
|
||||
|
||||
msgid "Create sub-bouquets"
|
||||
msgstr "Crea sotto-bouquet"
|
||||
|
||||
msgid "Add image"
|
||||
msgstr "Aggiungi immagine"
|
||||
|
||||
msgid "TV Format"
|
||||
msgstr "Formato TV"
|
||||
|
||||
msgid "Use with Streamrelay"
|
||||
msgstr "Usa con Streamrelay"
|
||||
|
||||
msgid "Remove use with Streamrelay"
|
||||
msgstr "Rimuovi usa con Streamrelay"
|
||||
|
||||
msgid "Enable additional name cache for EPG"
|
||||
msgstr "Abilita cache nomi aggiuntiva per EPG"
|
||||
|
||||
msgid ""
|
||||
"Enables additional cache to display EPG for some IPTV channels imported from *"
|
||||
".m3u."
|
||||
msgstr ""
|
||||
"Abilita una cache aggiuntiva per visualizzare l'EPG per alcuni canali IPTV imp"
|
||||
"ortati da *.m3u."
|
||||
|
||||
msgid "Enable deep name comparison"
|
||||
msgstr "Abilita confronto approfondito dei nomi"
|
||||
|
||||
msgid "Enables deeper name matching. Possible inaccuracies!"
|
||||
msgstr ""
|
||||
"Abilita una corrispondenza più approfondita dei nomi. Possibili imprecisioni!"
|
||||
|
||||
msgid "Convert for selected bouquets"
|
||||
msgstr "Converti per i bouquet selezionati"
|
||||
|
||||
msgid "There were errors [%s] during bouquets loading!"
|
||||
msgstr "Si sono verificati errori [%s] durante il caricamento dei bouquet!"
|
||||
|
||||
msgid "Check the log for more info."
|
||||
msgstr "Per maggiori informazioni consulta il log."
|
||||
|
||||
msgid "Satellite channel"
|
||||
msgstr "Canale satellitare"
|
||||
|
||||
msgid "Terrestrial channel"
|
||||
msgstr "Canale terrestre"
|
||||
|
||||
msgid "Cable channel"
|
||||
msgstr "Canale via cavo"
|
||||
|
||||
msgid "Save current changes"
|
||||
msgstr "Salva le modifiche attuali"
|
||||
|
||||
msgid "Create a new service"
|
||||
msgstr "Crea un nuovo servizio"
|
||||
|
||||
msgid "Extension support is disabled!"
|
||||
msgstr "Il supporto dell'estensione è disabilitato!"
|
||||
|
||||
msgid "Do you want to enable it?"
|
||||
msgstr "Vuoi abilitarlo?"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 Dmitriy Yefremov
|
||||
# Copyright (C) 2018-2026 Dmitriy Yefremov
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
#
|
||||
@@ -11,7 +11,7 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "translator-credits"
|
||||
msgstr ""
|
||||
msgstr "Дмитрий Ефремов"
|
||||
|
||||
# Main
|
||||
msgid "Service"
|
||||
@@ -258,8 +258,14 @@ msgid "Extra:"
|
||||
msgstr "Дополнительно:"
|
||||
|
||||
# Filter bar
|
||||
msgid "Only free"
|
||||
msgstr "Только открытые"
|
||||
msgid "Access"
|
||||
msgstr "Доступ"
|
||||
|
||||
msgid "Free (FTA)"
|
||||
msgstr "Oткрытые (FTA)"
|
||||
|
||||
msgid "Coded"
|
||||
msgstr "Закодированные"
|
||||
|
||||
msgid "All positions"
|
||||
msgstr "Все позиции"
|
||||
@@ -332,8 +338,8 @@ msgstr "Путь к пиконам формата Enigma2:"
|
||||
msgid "Specify the correct position value for the provider!"
|
||||
msgstr "Укажите правильное значение позиции для провайдера!"
|
||||
|
||||
msgid "Converter between name formats"
|
||||
msgstr "Конвертер формата имен"
|
||||
msgid "Converter between formats"
|
||||
msgstr "Конвертер форматов"
|
||||
|
||||
msgid "Receive picons for providers"
|
||||
msgstr "Получение пиконов для провайдеров"
|
||||
@@ -1539,3 +1545,69 @@ msgstr "Разбить по группам"
|
||||
|
||||
msgid "Create sub-bouquets"
|
||||
msgstr "Создать подбукеты"
|
||||
|
||||
msgid "Add image"
|
||||
msgstr "Добавить изображение"
|
||||
|
||||
msgid "TV Format"
|
||||
msgstr "ТВ-формат"
|
||||
|
||||
msgid "Use with Streamrelay"
|
||||
msgstr "Использовать Streamrelay"
|
||||
|
||||
msgid "Remove use with Streamrelay"
|
||||
msgstr "Не использовать Streamrelay"
|
||||
|
||||
msgid "Enable additional name cache for EPG"
|
||||
msgstr "Включить дополнительный кэш имен для EPG"
|
||||
|
||||
msgid "Enables additional cache to display EPG for some IPTV channels imported from *.m3u."
|
||||
msgstr "Включает дополнительный кэш для отображения EPG некоторых каналов IPTV импортированных из *.m3u."
|
||||
|
||||
msgid "Enable deep name comparison"
|
||||
msgstr "Включить глубокое сравнение имен"
|
||||
|
||||
msgid "Enables deeper name matching. Possible inaccuracies!"
|
||||
msgstr "Позволяет более глубокое сопоставление имен. Возможны неточности!"
|
||||
|
||||
msgid "Convert for selected bouquets"
|
||||
msgstr "Конвертировать для выбранных букетов"
|
||||
|
||||
msgid "There were errors [%s] during bouquets loading!"
|
||||
msgstr "При загрузке букетов возникли ошибки [%s]!"
|
||||
|
||||
msgid "Check the log for more info."
|
||||
msgstr "Смотрите журнал для расширенной информации."
|
||||
|
||||
msgid "Satellite channel"
|
||||
msgstr "Спутниковый канал"
|
||||
|
||||
msgid "Terrestrial channel"
|
||||
msgstr "Эфирный канал"
|
||||
|
||||
msgid "Cable channel"
|
||||
msgstr "Кабельный канал"
|
||||
|
||||
msgid "Save current changes"
|
||||
msgstr "Сохранить текущие изменения"
|
||||
|
||||
msgid "Create a new service"
|
||||
msgstr "Создать новый сервис"
|
||||
|
||||
msgid "Extension support is disabled!"
|
||||
msgstr "Поддержка расширений отключена!"
|
||||
|
||||
msgid "Do you want to enable it?"
|
||||
msgstr "Желаете включить?"
|
||||
|
||||
msgid "Playback of IPTV streams only!"
|
||||
msgstr "Воспроизведение только IPTV-потоков!"
|
||||
|
||||
msgid "There are running background tasks!"
|
||||
msgstr "Идет выполнение фоновых задач!"
|
||||
|
||||
msgid "Check if FFmpeg is installed!"
|
||||
msgstr "Проверьте, установлен ли FFmpeg!"
|
||||
|
||||
msgid "It can cause some problems."
|
||||
msgstr "Это может вызывать некоторые проблемы."
|
||||
|
||||
1603
po/sk/demon-editor.po
Normal file
1603
po/sk/demon-editor.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: DemonEditor\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-04-16 15:59+0300\n"
|
||||
"PO-Revision-Date: 2023-10-08 13:43+0300\n"
|
||||
"PO-Revision-Date: 2026-01-25 17:07+0300\n"
|
||||
"Last-Translator: audi06_19 <info@dreamosat-forum.com>\n"
|
||||
"Language-Team: audi06_19 <info@dreamosat-forum.com>\n"
|
||||
"Language: tr\n"
|
||||
@@ -11,7 +11,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 3.3.2\n"
|
||||
"X-Generator: Poedit 3.8\n"
|
||||
|
||||
msgid "translator-credits"
|
||||
msgstr "audi06_19 <info@dreamosat-forum.com>"
|
||||
@@ -261,8 +261,14 @@ msgid "Extra:"
|
||||
msgstr "Ekstra:"
|
||||
|
||||
# Filter bar
|
||||
msgid "Only free"
|
||||
msgstr "Sadece ücretsiz"
|
||||
msgid "Access"
|
||||
msgstr "Erişim"
|
||||
|
||||
msgid "Free (FTA)"
|
||||
msgstr "Açık (FTA)"
|
||||
|
||||
msgid "Coded"
|
||||
msgstr "Kodlanmış"
|
||||
|
||||
msgid "All positions"
|
||||
msgstr "Tüm pozisyonlar"
|
||||
@@ -335,8 +341,8 @@ msgstr "Enigma2 piconların yolu:"
|
||||
msgid "Specify the correct position value for the provider!"
|
||||
msgstr "Sağlayıcı için doğru pozisyon değerini belirtin!"
|
||||
|
||||
msgid "Converter between name formats"
|
||||
msgstr "İsim formatları arasında dönüştürücü"
|
||||
msgid "Converter between formats"
|
||||
msgstr "Biçimler arasında dönüştürücü"
|
||||
|
||||
msgid "Receive picons for providers"
|
||||
msgstr "Sağlayıcılar için picon alma"
|
||||
@@ -1534,3 +1540,96 @@ msgstr "Piconlar için ortak klasörü kullan"
|
||||
|
||||
msgid "Activates single folder use for several profiles."
|
||||
msgstr "Birden fazla profil için tek klasör kullanımını etkinleştirir."
|
||||
|
||||
msgid "Events"
|
||||
msgstr "Olaylar"
|
||||
|
||||
msgid "Markers"
|
||||
msgstr "İşaretleyiciler"
|
||||
|
||||
msgid "IPTV only"
|
||||
msgstr "Yalnızca IPTV"
|
||||
|
||||
msgid "No"
|
||||
msgstr "Hayır"
|
||||
|
||||
msgid "Not found."
|
||||
msgstr "Bulunamadı."
|
||||
|
||||
msgid "Current EPG cache contents."
|
||||
msgstr "Geçerli EPG önbellek içerikleri."
|
||||
|
||||
msgid "Source error!"
|
||||
msgstr "Kaynak hatası!"
|
||||
|
||||
msgid "The EPG source for the favorites list is not set!"
|
||||
msgstr "Favoriler listesi için EPG kaynağı ayarlanmamış!"
|
||||
|
||||
msgid "Add to EPG sources list"
|
||||
msgstr "EPG kaynakları listesine ekle"
|
||||
|
||||
msgid "Current bouquet"
|
||||
msgstr "Mevcut buket"
|
||||
|
||||
msgid "Single bouquet"
|
||||
msgstr "Tek buket"
|
||||
|
||||
msgid "Split by groups"
|
||||
msgstr "Gruplara göre böl"
|
||||
|
||||
msgid "Create sub-bouquets"
|
||||
msgstr "Alt buketler oluştur"
|
||||
|
||||
msgid "Add image"
|
||||
msgstr "Resim ekle"
|
||||
|
||||
msgid "TV Format"
|
||||
msgstr "TV Formatı"
|
||||
|
||||
msgid "Use with Streamrelay"
|
||||
msgstr "Streamrelay ile kullan"
|
||||
|
||||
msgid "Remove use with Streamrelay"
|
||||
msgstr "Streamrelay ile kullanımı kaldır"
|
||||
|
||||
msgid "Enable additional name cache for EPG"
|
||||
msgstr "EPG için ek ad önbelleğini etkinleştirin"
|
||||
|
||||
msgid "Enables additional cache to display EPG for some IPTV channels imported from *.m3u."
|
||||
msgstr "*.m3u'dan içe aktarılan bazı IPTV kanalları için EPG'yi görüntülemek üzere ek önbelleği etkinleştirir."
|
||||
|
||||
msgid "Enable deep name comparison"
|
||||
msgstr "Derin ad karşılaştırmasını etkinleştir"
|
||||
|
||||
msgid "Enables deeper name matching. Possible inaccuracies!"
|
||||
msgstr "Daha derin isim eşleştirmesini etkinleştirir. Olası yanlışlıklar!"
|
||||
|
||||
msgid "Convert for selected bouquets"
|
||||
msgstr "Seçili buketler için dönüştür"
|
||||
|
||||
msgid "There were errors [%s] during bouquets loading!"
|
||||
msgstr "Buketler yüklenirken [%s] hata oluştu!"
|
||||
|
||||
msgid "Check the log for more info."
|
||||
msgstr "Daha fazla bilgi için günlüğü kontrol edin."
|
||||
|
||||
msgid "Satellite channel"
|
||||
msgstr "Uydu kanalı"
|
||||
|
||||
msgid "Terrestrial channel"
|
||||
msgstr "Karasal kanal"
|
||||
|
||||
msgid "Cable channel"
|
||||
msgstr "Kablo kanalı"
|
||||
|
||||
msgid "Save current changes"
|
||||
msgstr "Mevcut değişiklikleri kaydet"
|
||||
|
||||
msgid "Create a new service"
|
||||
msgstr "Yeni bir hizmet oluşturun"
|
||||
|
||||
msgid "Extension support is disabled!"
|
||||
msgstr "Uzantı desteği devre dışı bırakıldı!"
|
||||
|
||||
msgid "Do you want to enable it?"
|
||||
msgstr "Etkinleştirmek ister misiniz?"
|
||||
|
||||
Reference in New Issue
Block a user