mirror of
https://github.com/DYefremov/DemonEditor.git
synced 2026-05-08 08:17:43 +02:00
Compare commits
300 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 | ||
|
|
8730cbdb7c | ||
|
|
cefb96ea20 | ||
|
|
b383c2572a | ||
|
|
ee2fcf8082 | ||
|
|
44234fa534 | ||
|
|
4bb66b5cc1 | ||
|
|
3b1ecbfbbf | ||
|
|
a7f7a59c8a | ||
|
|
9068028662 | ||
|
|
40fbc7809f | ||
|
|
6397c2c7f3 | ||
|
|
81e8e30682 | ||
|
|
9b4c6ab14a | ||
|
|
cad1437c33 | ||
|
|
4799a0d464 | ||
|
|
ce890353a4 | ||
|
|
506e07e3f4 | ||
|
|
e7a61e3f05 | ||
|
|
274abec3b8 | ||
|
|
77a5f55522 | ||
|
|
a7f334682f | ||
|
|
a9b9e5865e | ||
|
|
d59458a84b | ||
|
|
076bcb0cce | ||
|
|
8cbf03eb51 | ||
|
|
abed7bf9cb | ||
|
|
388e748673 | ||
|
|
ba3a3ae0aa | ||
|
|
9bb4f6d75d | ||
|
|
06242ce611 | ||
|
|
e4b1c98b2a | ||
|
|
6a8426e6ef | ||
|
|
583927f1b1 | ||
|
|
185a9b0082 | ||
|
|
ee7eb01af5 | ||
|
|
07c8034393 | ||
|
|
8ec73bc0f9 | ||
|
|
940b28ff6d | ||
|
|
316260703c | ||
|
|
e6c7b6572c | ||
|
|
0e50cf4927 | ||
|
|
f55dff1618 | ||
|
|
669916a5a9 | ||
|
|
4154f4d2f5 | ||
|
|
f3d133b7a3 | ||
|
|
a432da60e5 | ||
|
|
d34128b701 | ||
|
|
d16d0e62e2 | ||
|
|
1a6270478a | ||
|
|
4144e37049 | ||
|
|
61faced24d | ||
|
|
add73558f3 | ||
|
|
259f20794c | ||
|
|
afdb9a3c6b | ||
|
|
8581e7be1f | ||
|
|
5496aec95f | ||
|
|
5441bb0c02 | ||
|
|
3991d2935f | ||
|
|
c42115ba7d | ||
|
|
929987b2f1 | ||
|
|
9a0bead39c | ||
|
|
d18e9c116b | ||
|
|
e477e54dcd | ||
|
|
48b9cb23eb | ||
|
|
5db027c1cf | ||
|
|
128fd8a792 | ||
|
|
32043b7df0 | ||
|
|
bb847cd94c | ||
|
|
64b836363b | ||
|
|
5f34652905 | ||
|
|
32078bc7e3 | ||
|
|
9c582c26db | ||
|
|
aa5addb280 | ||
|
|
2d8ae1bbe2 | ||
|
|
5405d3a9a8 | ||
|
|
d91cbc395c | ||
|
|
e44aedebad | ||
|
|
9ccbd46b71 | ||
|
|
3f0e9e44a9 | ||
|
|
32a847fc5d | ||
|
|
795ad51098 | ||
|
|
64827a60d8 | ||
|
|
b98d55ffb6 | ||
|
|
aa21862303 | ||
|
|
f205e4fa66 | ||
|
|
4ffecb9ce4 | ||
|
|
ecc95e9c82 | ||
|
|
4c5cbcb514 | ||
|
|
8bd24e9642 | ||
|
|
64234c26e0 | ||
|
|
0b1d31fb8a | ||
|
|
a638a6b1fc |
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
|
||||
|
||||
24
README.md
24
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
|
||||
@@ -75,10 +75,13 @@ Users of **LTS** versions of [Ubuntu](https://ubuntu.com/) or distributions base
|
||||
A ready-made [package](https://aur.archlinux.org/packages/demoneditor-bin) is also available for [Arch Linux](https://archlinux.org/) users in the [AUR](https://aur.archlinux.org/) repository.
|
||||
* ### macOS
|
||||
**This program can be run on macOS.**
|
||||
To run the program on macOS, you need to install [brew](https://brew.sh/).
|
||||
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```
|
||||
```pip3 install requests, pillow```
|
||||
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme gtksourceview3```
|
||||
|
||||
```pip3 install requests telnetlib-313-and-up --break-system-packages```
|
||||
|
||||
*Optional:* ```brew install pillow python-chardet ffmpeg```
|
||||
|
||||
Launch is similar to Linux.
|
||||
|
||||
@@ -98,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-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
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
|
||||
""" Module for working with Enigma2 bouquets. """
|
||||
import os.path
|
||||
import re
|
||||
from collections import Counter
|
||||
from enum import Enum
|
||||
@@ -41,16 +42,34 @@ _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.
|
||||
|
||||
If "force_bq_names" then naming the files using the name of the bouquet.
|
||||
Some images may have problems displaying the favorites list!
|
||||
"""
|
||||
_SERVICE = '#SERVICE 1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
|
||||
_SERVICE = '#SERVICE 1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet\n'
|
||||
_MARKER = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n"
|
||||
_SPACE = "#SERVICE 1:832:D:{}:0:0:0:0:0:0:\n"
|
||||
_LOCKED = '1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet'
|
||||
_LOCKED = '1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet'
|
||||
_ALT = '#SERVICE 1:134:1:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet\n'
|
||||
_ALT_PAT = r"[<>:\"/\\|?*\-\s]"
|
||||
|
||||
@@ -76,18 +95,18 @@ class BouquetsWriter:
|
||||
m_count = 0
|
||||
|
||||
for bq in bqs.bouquets:
|
||||
bq_name = bq.file
|
||||
if not bq_name:
|
||||
if self._force_bq_names:
|
||||
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
|
||||
else:
|
||||
bq_name = f"de{count:02d}"
|
||||
while bq_name in bq_file_names:
|
||||
count += 1
|
||||
bq_name = f"de{count:02d}"
|
||||
bq_file_names.add(bq_name)
|
||||
|
||||
f_name = bq.file
|
||||
bq_type = BqType(bq.type)
|
||||
if not f_name:
|
||||
if self._force_bq_names or bq_type is BqType.BOUQUET:
|
||||
f_name = f"userbouquet.{re.sub(self._NAME_PATTERN, '_', bq.name)}.{bqs.type}"
|
||||
else:
|
||||
f_name = f"userbouquet.de{count:02d}.{bqs.type}"
|
||||
while f_name in bq_file_names:
|
||||
count += 1
|
||||
f_name = f"userbouquet.de{count:02d}.{bqs.type}"
|
||||
bq_file_names.add(f_name)
|
||||
|
||||
if bq_type is BqType.MARKER:
|
||||
m_data = bq.file.split(":") if bq.file else None
|
||||
b_name = m_data[-1].strip() if m_data else bq.name.lstrip(_MARKER_PREFIX)
|
||||
@@ -95,17 +114,16 @@ class BouquetsWriter:
|
||||
m_count += 1
|
||||
else:
|
||||
if bq_type is BqType.BOUQUET:
|
||||
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
|
||||
self.write_sub_bouquet(self._path, bq_name, bq, bqs.type)
|
||||
self.write_sub_bouquet(self._path, f_name, bq, bqs.type)
|
||||
else:
|
||||
self.write_bouquet(f"{self._path}userbouquet.{bq_name}.{bqs.type}", bq.name, bq.services)
|
||||
self.write_bouquet(f"{self._path}{f_name}", bq.name, bq.services)
|
||||
bq_type = 2 if bqs.type == BqType.RADIO.value else 1
|
||||
# Parental lock.
|
||||
locked = self._LOCKED.format(ServiceType.SERVICE, bq_type, bq_name, bqs.type)
|
||||
locked = self._LOCKED.format(ServiceType.SERVICE, bq_type, f_name)
|
||||
self._black_list.add(locked) if bq.locked else self._black_list.discard(locked)
|
||||
# Hiding.
|
||||
s_type = ServiceType.HIDDEN if bq.hidden else ServiceType.BOUQUET
|
||||
line.append(self._SERVICE.format(s_type, bq_type, bq_name, bqs.type))
|
||||
line.append(self._SERVICE.format(s_type, bq_type, f_name))
|
||||
|
||||
with open(f"{self._path}bouquets.{bqs.type}", "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(line)
|
||||
@@ -139,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)
|
||||
@@ -153,43 +170,30 @@ class BouquetsWriter:
|
||||
sb_type = 2 if bq_type == BqType.RADIO.value else 1
|
||||
|
||||
for sb in bq.services:
|
||||
bq_name = f"subbouquet.{re.sub(self._NAME_PATTERN, '_', sb.name)}.{sb.type}"
|
||||
self.write_bouquet(f"{path}{bq_name}", sb.name, sb.services)
|
||||
bouquet.append(f"#SERVICE 1:7:{sb_type}:0:0:0:0:0:0:0:FROM BOUQUET \"{bq_name}\" ORDER BY bouquet\n")
|
||||
sb_file = sb.file or f"subbouquet.{re.sub(self._NAME_PATTERN, '_', sb.name)}.{sb.type}"
|
||||
self.write_bouquet(f"{path}{sb_file}", sb.name, sb.services)
|
||||
bouquet.append(f"#SERVICE 1:7:{sb_type}:0:0:0:0:0:0:0:FROM BOUQUET \"{sb_file}\" ORDER BY bouquet\n")
|
||||
|
||||
with open(f"{self._path}userbouquet.{file_name}.{bq_type}", "w", encoding="utf-8", newline="\n") as file:
|
||||
with open(f"{self._path}{file_name}", "w", encoding="utf-8", newline="\n") as file:
|
||||
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. """
|
||||
_ALT_PAT = re.compile(r".*alternatives\.+(.*)\.([tv|radio]+).*")
|
||||
_BQ_PAT = re.compile(r".*\s+\W(.*bouquet)\.+(.*)\.+[tv|radio].*")
|
||||
_SUB_BQ_PAT = re.compile(r".*subbouquet\.+(.*)\.([tv|radio]+).*")
|
||||
_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. """
|
||||
@@ -201,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, [])
|
||||
|
||||
@@ -209,50 +214,71 @@ class BouquetsReader:
|
||||
|
||||
for line in file.readlines():
|
||||
if "#SERVICE" in line:
|
||||
name = re.match(self._BQ_PAT, line)
|
||||
s_data = line.split(":")
|
||||
s_type = ServiceType(s_data[1])
|
||||
if name:
|
||||
prefix, b_name = name.group(1), name.group(2)
|
||||
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)
|
||||
if mt:
|
||||
log(f"Warning: The bouquet file name may be formed incorrectly. -> {mt.group(1)}")
|
||||
|
||||
if mt:
|
||||
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:
|
||||
b_names.add(b_name)
|
||||
|
||||
rb_name, services = self.get_bouquet(self._path, b_name, bq_type, prefix)
|
||||
rb_name, services = self.get_bouquet(self._path, file_name, b_name)
|
||||
if rb_name in real_b_names:
|
||||
log(f"Bouquet file '{prefix}.{b_name}.{bq_type}' has duplicate name: {rb_name}")
|
||||
log(f"Bouquet file '{file_name}' has duplicate name: {rb_name}")
|
||||
real_b_names[rb_name] += 1
|
||||
rb_name = f"{rb_name} {real_b_names[rb_name]}"
|
||||
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, b_name))
|
||||
bouquets[2].append(Bouquet(rb_name, bq_type, services, locked, hidden, file_name))
|
||||
else:
|
||||
if len(s_data) == 12 and s_type is ServiceType.MARKER:
|
||||
b_name = f"{_MARKER_PREFIX}{s_data[-1].strip()}"
|
||||
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, bq_name, bq_type, prefix="userbouquet"):
|
||||
def get_bouquet(self, path, f_name, bq_name):
|
||||
""" Parsing services ids from bouquet file. """
|
||||
with open(f"{path}{prefix}.{bq_name}.{bq_type}", 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 'userbouquet.{bq_name}.{bq_type}' is empty or wrong!")
|
||||
log(f"Bouquet file '{f_name}' is empty or wrong!")
|
||||
self._errors += 1
|
||||
return f"{bq_name} [empty]", services
|
||||
|
||||
bq_name = srvs.pop(0)
|
||||
@@ -262,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._ALT_PAT, srv)
|
||||
alt = re.match(self._BQ_PAT, srv)
|
||||
if alt:
|
||||
alt_name, alt_type = alt.group(1), alt.group(2)
|
||||
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, alt_name, alt_type, "alternatives")
|
||||
af_name, alt_name = alt.group(1), alt.group(3)
|
||||
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._SUB_BQ_PAT, srv)
|
||||
sub = re.match(self._BQ_PAT, srv)
|
||||
if sub:
|
||||
sub_name, sub_type = sub.group(1), sub.group(2)
|
||||
sub_bq_name, sub_srvs = BouquetsReader.get_bouquet(path, sub_name, sub_type, "subbouquet")
|
||||
bq = Bouquet(sub_bq_name, sub_type, tuple(sub_srvs), None, None, sub_name)
|
||||
sf_name, sub_name, sub_type = sub.group(1), sub.group(3), sub.group(4)
|
||||
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-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
|
||||
@@ -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"
|
||||
@@ -58,6 +60,9 @@ class StreamType(Enum):
|
||||
|
||||
|
||||
def parse_m3u(path, s_type, detect_encoding=True, params=None):
|
||||
""" Parses *m3u* file and returns tuple with EPG src URLs and services list. """
|
||||
pattern = re.compile(r'(\S+)="(.*?)"')
|
||||
|
||||
with open(path, "rb") as file:
|
||||
data = file.read()
|
||||
encoding = "utf-8"
|
||||
@@ -70,11 +75,14 @@ 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]
|
||||
services = []
|
||||
epg_src = None
|
||||
group = None
|
||||
groups = set()
|
||||
services = []
|
||||
marker_counter = 1
|
||||
sid_counter = 1
|
||||
name = None
|
||||
@@ -85,66 +93,68 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
|
||||
m_name = BqServiceType.MARKER.name
|
||||
|
||||
for line in str(data, encoding=encoding, errors="ignore").splitlines():
|
||||
if line.startswith("#EXTM3U"):
|
||||
data = dict(pattern.findall(line))
|
||||
epg_src = data.get("x-tvg-url", data.get("url-tvg", None))
|
||||
epg_src = epg_src.split(",") if epg_src else None
|
||||
if line.startswith("#EXTINF"):
|
||||
line, sep, name = line.rpartition(",")
|
||||
|
||||
data = re.split('"', line)
|
||||
size = len(data)
|
||||
if size < 3:
|
||||
continue
|
||||
d = {data[i].lower().strip(" ="): data[i + 1] for i in range(0, len(data) - 1, 2)}
|
||||
picon = d.get("tvg-logo", 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:
|
||||
grp_name = d.get("group-title", None)
|
||||
if grp_name not in groups:
|
||||
groups.add(grp_name)
|
||||
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
|
||||
marker_counter += 1
|
||||
mr = Service(None, None, None, grp_name, *aggr[0:3], m_name, *aggr, fav_id, None)
|
||||
services.append(mr)
|
||||
group = data.get("group-title", None)
|
||||
elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2:
|
||||
grp_name = line.strip("#EXTGRP:").strip()
|
||||
if grp_name not in groups:
|
||||
groups.add(grp_name)
|
||||
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
|
||||
marker_counter += 1
|
||||
mr = Service(None, None, None, grp_name, *aggr[0:3], m_name, *aggr, fav_id, None)
|
||||
services.append(mr)
|
||||
elif not line.startswith("#"):
|
||||
group = line.strip("#EXTGRP:").strip()
|
||||
elif not line.startswith("#") and "://" in line:
|
||||
url = line.strip()
|
||||
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:
|
||||
# Some playlists have "random" of group names.
|
||||
# We will take only the first one we found on the list!
|
||||
groups.add(group)
|
||||
m_id = MARKER_FORMAT.format(marker_counter, group, group)
|
||||
marker_counter += 1
|
||||
services.append(Service(None, None, None, group, *aggr[0:3], m_name, *aggr, m_id, None))
|
||||
|
||||
if all((name, url, fav_id)):
|
||||
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], st, picon, p_id, *s_aggr, url, fav_id, None)
|
||||
services.append(srv)
|
||||
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}]")
|
||||
|
||||
return services
|
||||
return epg_src, services
|
||||
|
||||
|
||||
def export_to_m3u(path, bouquet, s_type, url=None):
|
||||
pattern = re.compile(".*:(http.*):.*") if s_type is SettingsType.ENIGMA_2 else re.compile("(http.*?)::::.*")
|
||||
pattern = re.compile(".*:(http.*).*") if s_type is SettingsType.ENIGMA_2 else re.compile("(http.*?)::::.*")
|
||||
lines = ["#EXTM3U\n"]
|
||||
current_grp = None
|
||||
|
||||
for s in bouquet.services:
|
||||
s_type = s.type
|
||||
if s_type is BqServiceType.IPTV:
|
||||
srv_type = s.type
|
||||
if srv_type is BqServiceType.IPTV:
|
||||
res = re.match(pattern, s.data)
|
||||
if not res:
|
||||
continue
|
||||
lines.append(f"#EXTINF:-1,{s.name}\n")
|
||||
lines.append(current_grp) if current_grp else None
|
||||
lines.append(f"{unquote(res.group(1).strip())}\n")
|
||||
elif s_type is BqServiceType.MARKER:
|
||||
u = res.group(1)
|
||||
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 s_type is BqServiceType.DEFAULT and url:
|
||||
elif srv_type is BqServiceType.DEFAULT and url:
|
||||
lines.append(f"#EXTINF:-1,{s.name}\n")
|
||||
lines.append(current_grp) if current_grp else None
|
||||
lines.append(f"{url}{s.data}\n")
|
||||
|
||||
@@ -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
|
||||
@@ -37,6 +37,7 @@ from ..ecommons import Bouquets, Bouquet, BouquetService, BqServiceType, PROVIDE
|
||||
_FILE = "bouquets.xml"
|
||||
_U_FILE = "ubouquets.xml"
|
||||
_W_FILE = "webtv_usr.xml"
|
||||
_WEB_TV_NAME = "[Web TV]"
|
||||
|
||||
_COMMENT = " File was created in DemonEditor. Enjoy watching! "
|
||||
|
||||
@@ -100,13 +101,20 @@ def parse_webtv(path, name, bq_type):
|
||||
return bouquets
|
||||
|
||||
dom = XmlHandler.parse(path)
|
||||
# Display name.
|
||||
name = None
|
||||
for e in dom.childNodes:
|
||||
if e.nodeType == e.ELEMENT_NODE:
|
||||
name = e.getAttribute("name")
|
||||
break
|
||||
|
||||
services = []
|
||||
for elem in dom.getElementsByTagName("webtv"):
|
||||
if elem.hasAttributes():
|
||||
web_attrs = get_xml_attributes(elem)
|
||||
services.append(get_webtv_service(web_attrs))
|
||||
|
||||
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None, file=None)
|
||||
bouquet = Bouquet(name=name or _WEB_TV_NAME, type=bq_type, services=services, locked=None, hidden=None, file=None)
|
||||
bouquets[2].append(bouquet)
|
||||
|
||||
return bouquets
|
||||
@@ -195,6 +203,7 @@ def write_webtv(file, bouquet):
|
||||
doc.appendChild(comment)
|
||||
|
||||
for bq in bouquet.bouquets:
|
||||
root.setAttribute("name", bq.name or _WEB_TV_NAME)
|
||||
for srv in bq.services:
|
||||
url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group = srv.fav_id.split("::")
|
||||
srv_elem = doc.createElement("webtv")
|
||||
|
||||
@@ -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
|
||||
@@ -596,6 +596,23 @@ class Settings:
|
||||
def epg_xml_source(self, value):
|
||||
self._cp_settings["epg_xml_source"] = value
|
||||
|
||||
@property
|
||||
def epg_xml_sources(self):
|
||||
return self._cp_settings.get("epg_xml_sources", [self.epg_xml_source])
|
||||
|
||||
@epg_xml_sources.setter
|
||||
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
|
||||
|
||||
164
app/tools/epg.py
164
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,9 +32,8 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
import struct
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import namedtuple
|
||||
from collections import namedtuple, defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from tempfile import NamedTemporaryFile
|
||||
from urllib.parse import urlparse
|
||||
@@ -61,6 +60,10 @@ EpgEvent.__new__.__defaults__ = ("N/A", "N/A", 0, 0, 0, "N/A", None) # For Pyth
|
||||
|
||||
class Reader(metaclass=abc.ABCMeta):
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def cache(self) -> dict: pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def download(self, clb=None): pass
|
||||
|
||||
@@ -121,6 +124,10 @@ class EPG:
|
||||
self._refs = {}
|
||||
self._desc = {}
|
||||
|
||||
@property
|
||||
def cache(self) -> dict:
|
||||
return self._refs
|
||||
|
||||
def download(self, clb=None):
|
||||
pass
|
||||
|
||||
@@ -229,13 +236,19 @@ class XmlTvReader(Reader):
|
||||
|
||||
TIME_FORMAT_STR = "%Y%m%d%H%M%S %z"
|
||||
|
||||
SUFFIXES = {".gz", ".xz", ".lzma", ".xml"}
|
||||
|
||||
Service = namedtuple("Service", ["id", "names", "logo", "events"])
|
||||
Event = namedtuple("EpgEvent", ["start", "duration", "title", "desc"])
|
||||
|
||||
def __init__(self, path, url):
|
||||
def __init__(self, path, url=None):
|
||||
self._path = path
|
||||
self._url = url
|
||||
self._ids = {}
|
||||
self._cache = {}
|
||||
|
||||
@property
|
||||
def cache(self) -> dict:
|
||||
return self._cache
|
||||
|
||||
def download(self, clb=None):
|
||||
""" Downloads an XMLTV file. """
|
||||
@@ -244,84 +257,115 @@ class XmlTvReader(Reader):
|
||||
log(f"{self.__class__.__name__} [download] error: Invalid URL {self._url}")
|
||||
return
|
||||
|
||||
with requests.get(url=self._url, stream=True) as request:
|
||||
if request.reason == "OK":
|
||||
suf = self._url[self._url.rfind("."):]
|
||||
if suf not in (".gz", ".xz", ".lzma"):
|
||||
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_len = request.headers.get("content-length")
|
||||
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_len = int(data_len)
|
||||
log("Downloading XMLTV file...")
|
||||
for data in request.iter_content(chunk_size=1024):
|
||||
downloaded += len(data)
|
||||
tf.write(data)
|
||||
done = int(50 * downloaded / data_len)
|
||||
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}")
|
||||
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: {request.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()
|
||||
|
||||
def get_current_events(self, names: set) -> dict:
|
||||
events = {}
|
||||
events = defaultdict(list)
|
||||
|
||||
dt = datetime.utcnow()
|
||||
utc = dt.timestamp()
|
||||
offset = datetime.now() - dt
|
||||
|
||||
for srv in filter(lambda s: any(name in names for name in s.names), self._ids.values()):
|
||||
ev = max(filter(lambda s: s.start < utc, srv.events), key=lambda x: x.start, default=None)
|
||||
if ev:
|
||||
start = datetime.fromtimestamp(ev.start) + offset
|
||||
end_time = datetime.fromtimestamp(ev.duration) + offset
|
||||
start = start.timestamp()
|
||||
end_time = end_time.timestamp()
|
||||
|
||||
for n in srv.names:
|
||||
events[n] = EpgEvent(n, ev.title, start, end_time, int(ev.duration), ev.desc, ev)
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def process_event(ev, events, offset, srv):
|
||||
start = datetime.fromtimestamp(ev.start) + offset
|
||||
end_time = datetime.fromtimestamp(ev.duration) + offset
|
||||
start = start.timestamp()
|
||||
end_time = end_time.timestamp()
|
||||
duration = end_time - start
|
||||
|
||||
for n in srv.names:
|
||||
data = {"e2eventservicename": n,
|
||||
"e2eventtitle": ev.title,
|
||||
"e2eventdescription": ev.desc,
|
||||
"e2eventstart": start,
|
||||
"e2eventduration": duration}
|
||||
events[n].append(EpgEvent(n, ev.title, start, end_time, duration, ev.desc, data))
|
||||
|
||||
def parse(self):
|
||||
""" Parses XML. """
|
||||
try:
|
||||
import gzip
|
||||
log("Processing XMLTV data...")
|
||||
suf = os.path.splitext(self._path)[1]
|
||||
if suf == ".gz":
|
||||
import gzip
|
||||
|
||||
with gzip.open(self._path, "rb") as gzf:
|
||||
log("Processing XMLTV data...")
|
||||
list(map(self.process_node, ET.iterparse(gzf)))
|
||||
log("XMLTV data parsing is complete.")
|
||||
with gzip.open(self._path, "rb") as gzf:
|
||||
list(map(self.process_node, ET.iterparse(gzf)))
|
||||
elif suf == ".xml":
|
||||
with open(self._path, "rb") as xml:
|
||||
list(map(self.process_node, ET.iterparse(xml)))
|
||||
else:
|
||||
log(f"{self.__class__.__name__} [parse] error: Unsupported file type [{suf}].")
|
||||
except OSError as e:
|
||||
log(f"{self.__class__.__name__} [parse] error: {e}")
|
||||
else:
|
||||
log("XMLTV data parsing is complete.")
|
||||
|
||||
def process_node(self, node):
|
||||
event, element = node
|
||||
@@ -329,9 +373,9 @@ class XmlTvReader(Reader):
|
||||
ch_id = element.get("id", None)
|
||||
logo = None # Currently not in use.
|
||||
# Since a service can have several names, we will store a set of names in the "names" field!
|
||||
self._ids[ch_id] = self.Service(ch_id, {c.text for c in element if c.tag == self.DSP_NAME_TAG}, logo, [])
|
||||
self._cache[ch_id] = self.Service(ch_id, {c.text for c in element if c.tag == self.DSP_NAME_TAG}, logo, [])
|
||||
elif element.tag == self.PR_TAG:
|
||||
channel = self._ids.get(element.get(self.CH_TAG, None), None)
|
||||
channel = self._cache.get(element.get(self.CH_TAG, None), None)
|
||||
if channel:
|
||||
events = channel[-1]
|
||||
start = element.get("start", None)
|
||||
|
||||
@@ -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>
|
||||
@@ -424,14 +434,6 @@
|
||||
<attribute name="label" translatable="yes">Import YouTube playlist</attribute>
|
||||
<attribute name="action">app.on_import_yt_list</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import m3u</attribute>
|
||||
<attribute name="action">app.on_import_m3u</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Export to m3u</attribute>
|
||||
<attribute name="action">app.on_export_iptv_to_m3u</attribute>
|
||||
</item>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">EPG configuration</attribute>
|
||||
|
||||
@@ -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,9 +38,15 @@ from pathlib import Path
|
||||
from app.commons import run_idle, get_size_from_bytes
|
||||
from app.settings import SettingsType, SEP
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_builder
|
||||
from app.ui.main_helper import append_text_to_tview
|
||||
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
|
||||
@@ -153,9 +159,7 @@ class BackupDialog:
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
@@ -219,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:
|
||||
@@ -234,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)
|
||||
@@ -261,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-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,7 +31,7 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor. -->
|
||||
<!-- interface-copyright 2018-2023 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.8.0 Alpha</property>
|
||||
<property name="copyright">2018-2023 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,20 +34,39 @@ 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
|
||||
|
||||
|
||||
class BaseDialog(Gtk.Dialog):
|
||||
""" Base dialog class for editing DVB (-> *.xml) data. """
|
||||
DEFAULT_BUTTONS = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK)
|
||||
|
||||
def __init__(self, parent, title, buttons=None, *args, **kwargs):
|
||||
super().__init__(transient_for=parent,
|
||||
title=translate(title),
|
||||
modal=True,
|
||||
resizable=False,
|
||||
default_width=255,
|
||||
skip_taskbar_hint=True,
|
||||
skip_pager_hint=True,
|
||||
destroy_with_parent=True,
|
||||
use_header_bar=USE_HEADER_BAR,
|
||||
window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
|
||||
buttons=buttons or self.DEFAULT_BUTTONS,
|
||||
*args, **kwargs)
|
||||
|
||||
|
||||
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>
|
||||
@@ -82,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)
|
||||
@@ -91,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):
|
||||
@@ -209,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:
|
||||
@@ -238,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
|
||||
"""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
<!-- Generated with glade 3.38.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -26,33 +26,33 @@ THE SOFTWARE.
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.22"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkAdjustment" id="interval_adjustment">
|
||||
<property name="lower">3</property>
|
||||
<property name="upper">60</property>
|
||||
<property name="value">3</property>
|
||||
<property name="step_increment">1</property>
|
||||
<property name="page_increment">10</property>
|
||||
<property name="step-increment">1</property>
|
||||
<property name="page-increment">10</property>
|
||||
</object>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<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">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<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="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<object class="GtkLabel" id="src_label">
|
||||
<property name="visible" bind-source="source_selection_box" bind-property="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="label" translatable="yes">Source:</property>
|
||||
</object>
|
||||
@@ -65,22 +65,22 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkBox" id="src_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="source_selection_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="http_src_button">
|
||||
<property name="label" translatable="yes">Receiver</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">dat_src_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -93,10 +93,10 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkRadioButton" id="xml_src_button">
|
||||
<property name="label" translatable="yes">XML TV</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">dat_src_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -108,12 +108,11 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="dat_src_button">
|
||||
<property name="label" translatable="yes">*.dat file</property>
|
||||
<property name="visible">False</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<property name="draw-indicator">False</property>
|
||||
<property name="group">http_src_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -132,12 +131,12 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkBox" id="interval_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="interval_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Update interval (sec):</property>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -149,10 +148,10 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="interval_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="max_width_chars">4</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="max-width-chars">4</property>
|
||||
<property name="adjustment">interval_adjustment</property>
|
||||
<property name="climb_rate">1</property>
|
||||
<property name="climb-rate">1</property>
|
||||
<property name="numeric">True</property>
|
||||
<property name="value">3</property>
|
||||
</object>
|
||||
@@ -172,15 +171,14 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkBox" id="xml_source_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="sensitive" bind-source="xml_src_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="sensitive" bind-source="xml_src_button" bind-property="active"/>
|
||||
<child>
|
||||
<object class="GtkLabel" id="url_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Url to *.xml.gz file:</property>
|
||||
</object>
|
||||
@@ -191,15 +189,130 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="url_entry">
|
||||
<object class="GtkBox" id="url_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">network-transmit-receive-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="input_purpose">url</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="url_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="active">0</property>
|
||||
<property name="has-entry">True</property>
|
||||
<child internal-child="entry">
|
||||
<object class="GtkEntry" id="url_entry">
|
||||
<property name="can-focus">False</property>
|
||||
<signal name="focus-out-event" handler="on_url_entry_focus_out" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="url_action_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="remove_url_button">
|
||||
<property name="visible" bind-source="apply_url_button" bind-property="visible" bind-flags="invert-boolean">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">Remove</property>
|
||||
<signal name="clicked" handler="on_remove_url" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="remove_url_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">list-remove-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="add_url_button">
|
||||
<property name="visible" bind-source="apply_url_button" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Add</property>
|
||||
<signal name="clicked" handler="on_add_url" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="add_url_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_url_button">
|
||||
<property name="visible" bind-source="apply_url_button" bind-property="visible">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Cancel</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="cancel_url_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-undo-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="apply_url_button">
|
||||
<property name="visible" bind-source="url_entry" bind-property="has-focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Add</property>
|
||||
<signal name="clicked" handler="on_apply_url" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="apply_url_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Apply</property>
|
||||
<property name="icon-name">insert-link-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</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">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
@@ -207,12 +320,12 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkBox" id="download_interval_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="download_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Update:</property>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -224,11 +337,11 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="download_interval_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="active">0</property>
|
||||
<property name="active_id">daily</property>
|
||||
<property name="active-id">daily</property>
|
||||
<items>
|
||||
<item id="daily" translatable="yes">Daily</item>
|
||||
</items>
|
||||
@@ -243,7 +356,7 @@ Author: Dmitriy Yefremov
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
@@ -255,16 +368,14 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="dat_source_box">
|
||||
<property name="visible">False</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="sensitive" bind-source="dat_src_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="sensitive" bind-source="dat_src_button" bind-property="active"/>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">STB path:</property>
|
||||
</object>
|
||||
@@ -277,9 +388,9 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="dat_path_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="active">0</property>
|
||||
<property name="active_id">/etc/enigma2</property>
|
||||
<property name="active-id">/etc/enigma2</property>
|
||||
<items>
|
||||
<item id="/etc/enigma2/">/etc/enigma2/</item>
|
||||
<item id="/media/hdd/">/media/hdd/</item>
|
||||
@@ -311,18 +422,18 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkButtonBox" 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="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>
|
||||
<property name="layout_style">expand</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="apply_button">
|
||||
<property name="label" translatable="yes">Apply</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_apply" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -335,8 +446,8 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkButton" id="close_button">
|
||||
<property name="label" translatable="yes">Close</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_close" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
|
||||
@@ -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
|
||||
@@ -84,44 +84,51 @@ Author: Dmitriy Yefremov
|
||||
<property name="margin-top">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child type="center">
|
||||
<object class="GtkToggleButton" id="multi_epg_button">
|
||||
<property name="label" translatable="yes">Multi EPG</property>
|
||||
<property name="name">header-button</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="toggled" handler="on_multi_epg_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="src_combo_box">
|
||||
<object class="GtkButtonBox" id="src_box">
|
||||
<property name="name">header-stack-switcher</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">EPG source</property>
|
||||
<property name="active">0</property>
|
||||
<property name="has-entry">True</property>
|
||||
<property name="active-id">0</property>
|
||||
<items>
|
||||
<item id="0" translatable="yes">Receiver</item>
|
||||
</items>
|
||||
<child internal-child="entry">
|
||||
<object class="GtkEntry">
|
||||
<property name="name">header-entry</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="width-chars">10</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="src_receiver_button">
|
||||
<property name="label" translatable="yes">Receiver</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>
|
||||
<property name="group">src_xmltv_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="src_xmltv_button">
|
||||
<property name="label" translatable="yes">XML TV</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>
|
||||
<property name="group">src_receiver_button</property>
|
||||
<signal name="toggled" handler="on_xmltv_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</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>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
@@ -143,12 +150,13 @@ Author: Dmitriy Yefremov
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="epg_add_timer_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive" bind-source="src_xmltv_button" bind-property="active" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
@@ -163,12 +171,55 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="multi_epg_button">
|
||||
<property name="name">header-button</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Multi EPG</property>
|
||||
<signal name="toggled" handler="on_multi_epg_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="multi_epg_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-select-all-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="epg_options_button">
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Options</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="epg_options_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">applications-system-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -327,6 +378,7 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_title_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
@@ -455,6 +507,44 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_cache_info_box">
|
||||
<property name="visible" bind-source="src_xmltv_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Current EPG cache contents.</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-select-all-symbolic</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="cache_info_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label">0</property>
|
||||
</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="pack-type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2023 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
|
||||
@@ -25,9 +25,8 @@
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import json
|
||||
import os
|
||||
import pkgutil
|
||||
import shutil
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
@@ -36,17 +35,20 @@ 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/"
|
||||
EXT_LIST_FILE = "https://raw.githubusercontent.com/DYefremov/demoneditor-extensions/main/extensions/extension-list"
|
||||
# Config file name. The config file must be in json format!
|
||||
# E.g. -> {"EXT_URL": "repo URL", "EXT_LIST_FILE": "URL to 'extension-list' file."}
|
||||
EXT_CONFIG_FILE = "ext_sources"
|
||||
HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i686; rv:112.0) Gecko/20100101 Firefox/112.0",
|
||||
"Accept": "application/json"}
|
||||
|
||||
|
||||
class ExtensionManager(Gtk.Window):
|
||||
ICON_INFO = "emblem-important-symbolic"
|
||||
ICON_INFO = "emblem-synchronizing-symbolic"
|
||||
ICON_UPDATE = "network-receive-symbolic"
|
||||
|
||||
class Column(IntEnum):
|
||||
@@ -130,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)
|
||||
@@ -140,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)
|
||||
@@ -168,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)
|
||||
@@ -183,35 +192,79 @@ class ExtensionManager(Gtk.Window):
|
||||
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)
|
||||
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()
|
||||
scf = f"{os.path.dirname(__file__)}{os.sep}{EXT_CONFIG_FILE}"
|
||||
if os.path.isfile(scf):
|
||||
with (open(scf, "r", encoding="utf-8", errors="ignore") as cf):
|
||||
config = json.load(cf)
|
||||
global EXT_URL, EXT_LIST_FILE
|
||||
EXT_URL = config.get("EXT_URL", EXT_URL)
|
||||
EXT_LIST_FILE = config.get("EXT_LIST_FILE", EXT_LIST_FILE)
|
||||
|
||||
self.update()
|
||||
|
||||
def get_installed(self):
|
||||
import pkgutil
|
||||
from importlib.util import module_from_spec
|
||||
|
||||
ext_paths = [f"{os.path.dirname(__file__)}{os.sep}", self._ext_path, "extensions"]
|
||||
installed = {}
|
||||
|
||||
for importer, name, is_package in pkgutil.iter_modules(ext_paths):
|
||||
if is_package:
|
||||
m = importer.find_module(name).load_module()
|
||||
spec = importer.find_spec(name)
|
||||
if spec is None:
|
||||
log(f"{self.__class__.__name__} [get installed]: Module {name} not found.")
|
||||
continue
|
||||
|
||||
m = module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
cls_name = name.capitalize()
|
||||
if hasattr(m, cls_name):
|
||||
cls = getattr(m, cls_name)
|
||||
path = Path(importer.find_module(name).path).parent
|
||||
path = Path(spec.origin).parent
|
||||
installed[name] = (cls, path)
|
||||
|
||||
return installed
|
||||
|
||||
@run_task
|
||||
def update(self):
|
||||
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:
|
||||
log(f"{self.__class__.__name__} [update] error: {e}")
|
||||
else:
|
||||
log(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)
|
||||
self.update_local_data()
|
||||
|
||||
@run_idle
|
||||
def update_data(self, data):
|
||||
@@ -219,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():
|
||||
@@ -234,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
|
||||
|
||||
@@ -276,7 +340,7 @@ class ExtensionManager(Gtk.Window):
|
||||
try:
|
||||
for f in resp.json():
|
||||
url = f.get("download_url", None)
|
||||
ver = f.get("version", "1.0")
|
||||
ver = f.get("version", ver)
|
||||
if url:
|
||||
urls[url] = f.get("name", None)
|
||||
except ValueError as e:
|
||||
|
||||
@@ -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-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
|
||||
@@ -36,7 +36,7 @@ from app.eparser.ecommons import BqType, BqServiceType, Bouquet
|
||||
from app.eparser.neutrino.bouquets import parse_webtv, parse_bouquets as get_neutrino_bouquets
|
||||
from app.settings import SettingsType, IS_DARWIN, SEP
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, translate, get_builder
|
||||
from app.ui.main_helper import on_popup_menu, get_iptv_data
|
||||
from app.ui.main_helper import on_popup_menu, get_iptv_data, show_info_bar_message
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, Column, Page, HeaderBar
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ def import_bouquet(app, model, path, appender, file_path=None):
|
||||
|
||||
if profile is SettingsType.ENIGMA_2:
|
||||
pattern = f".{bq_type.value}"
|
||||
f_pattern = f"{'' if IS_DARWIN else 'userbouquet.'}*{pattern}"
|
||||
f_pattern = f"*{pattern}"
|
||||
elif profile is SettingsType.NEUTRINO_MP:
|
||||
pattern = "webtv.xml" if bq_type is BqType.WEBTV else "bouquets.xml"
|
||||
f_pattern = "bouquets.xml"
|
||||
@@ -96,10 +96,9 @@ def import_bouquet(app, model, path, appender, file_path=None):
|
||||
|
||||
|
||||
def get_enigma2_bouquet(path):
|
||||
path, sep, f_name = path.rpartition("userbouquet.")
|
||||
name, sep, suf = f_name.rpartition(".")
|
||||
bq = BouquetsReader.get_bouquet(path, name, suf)
|
||||
bouquet = Bouquet(name=bq[0], type=BqType(suf).value, services=bq[1], locked=None, hidden=None)
|
||||
p = Path(path)
|
||||
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
|
||||
|
||||
|
||||
@@ -195,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))
|
||||
@@ -374,9 +377,7 @@ class ImportDialog:
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
|
||||
|
||||
@run_idle
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
@@ -425,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>
|
||||
@@ -933,6 +935,8 @@ Author: Dmitriy Yefremov
|
||||
<property name="can-focus">False</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="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
|
||||
342
app/ui/iptv.py
342
app/ui/iptv.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
|
||||
@@ -30,6 +30,8 @@ import concurrent.futures
|
||||
import os
|
||||
import re
|
||||
import urllib
|
||||
from datetime import date
|
||||
from itertools import groupby, chain
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import urlparse, unquote, quote
|
||||
from urllib.request import Request, urlopen
|
||||
@@ -38,19 +40,21 @@ import requests
|
||||
from gi.repository import GLib, Gio, GdkPixbuf
|
||||
|
||||
from app.commons import run_idle, run_task, log
|
||||
from app.eparser.ecommons import BqServiceType, Service
|
||||
from app.eparser.ecommons import BqServiceType, BouquetService, Service
|
||||
from app.eparser.iptv import (NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT,
|
||||
parse_m3u, PICON_FORMAT)
|
||||
from app.settings import SettingsType
|
||||
from app.tools.yt import YouTubeException, YouTube
|
||||
from app.ui.dialogs import Action, show_dialog, DialogType, translate, get_builder
|
||||
from app.ui.main_helper import get_iptv_url, on_popup_menu, get_picon_pixbuf
|
||||
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)
|
||||
|
||||
_DIGIT_ENTRY_NAME = "digit-entry"
|
||||
_ENIGMA2_REFERENCE = "{}:{}:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0"
|
||||
_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
|
||||
_UI_PATH = UI_RESOURCES_PATH + "iptv.glade"
|
||||
_UI_PATH = f"{UI_RESOURCES_PATH}iptv.glade"
|
||||
_CSS_PATH = f"{UI_RESOURCES_PATH}style.css"
|
||||
_URL_PREFIXES = {"YT-DLP": "YT-DLP://", "YT-DL": "YT-DL://", "STREAMLINK": "streamlink://", "No": None}
|
||||
|
||||
|
||||
@@ -124,7 +128,7 @@ class IptvDialog:
|
||||
self._model, self._paths = view.get_selection().get_selected_rows()
|
||||
# Style.
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._style_provider.load_from_path(_CSS_PATH)
|
||||
self._digit_elems = (self._srv_id_entry, self._srv_type_entry, self._sid_entry, self._tr_id_entry,
|
||||
self._net_id_entry, self._namespace_entry)
|
||||
for el in self._digit_elems:
|
||||
@@ -163,14 +167,14 @@ class IptvDialog:
|
||||
self.on_url_changed(self._url_entry)
|
||||
|
||||
if not is_data_correct(self._digit_elems) or self._url_entry.get_name() == _DIGIT_ENTRY_NAME:
|
||||
self.show_info_message(translate("Error. Verify the data!"), Gtk.MessageType.ERROR)
|
||||
self.show_info_message("Error. Verify the data!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
url = self._url_entry.get_text()
|
||||
if all((self._url_prefix_box.get_visible(),
|
||||
self._url_prefix_combobox.get_active_id(),
|
||||
url.count("http") > 1 or urlparse(url).scheme.upper() in _URL_PREFIXES)):
|
||||
self.show_info_message(translate("Invalid prefix for the given URL!"), Gtk.MessageType.ERROR)
|
||||
self.show_info_message("Invalid prefix for the given URL!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
@@ -247,11 +251,8 @@ class IptvDialog:
|
||||
return get_stream_type(self._stream_type_combobox)
|
||||
|
||||
def on_entry_changed(self, entry):
|
||||
if _PATTERN.search(entry.get_text()):
|
||||
entry.set_name(_DIGIT_ENTRY_NAME)
|
||||
else:
|
||||
entry.set_name("GtkEntry")
|
||||
self.update_reference_entry()
|
||||
entry.set_name(_DIGIT_ENTRY_NAME if _PATTERN.search(entry.get_text()) else "GtkEntry")
|
||||
self.update_reference_entry()
|
||||
|
||||
def on_url_changed(self, entry):
|
||||
url_str = entry.get_text()
|
||||
@@ -390,9 +391,7 @@ class IptvDialog:
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
|
||||
|
||||
|
||||
class SearchUnavailableDialog:
|
||||
@@ -509,6 +508,7 @@ class IptvListDialog:
|
||||
self._data_box = builder.get_object("iptv_list_data_box")
|
||||
self._start_values_grid = builder.get_object("start_values_grid")
|
||||
self._info_bar = builder.get_object("list_configuration_info_bar")
|
||||
self._message_label = builder.get_object("list_configuration_message_label")
|
||||
self._reference_label = builder.get_object("reference_label")
|
||||
self._stream_type_check_button = builder.get_object("stream_type_default_check_button")
|
||||
self._id_default_check_button = builder.get_object("id_default_check_button")
|
||||
@@ -531,7 +531,7 @@ class IptvListDialog:
|
||||
self._ok_button.bind_property("visible", self._cancel_button, "visible", 4)
|
||||
# Style
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
style_provider.load_from_path(_CSS_PATH)
|
||||
self._default_elems = (self._stream_type_check_button, self._id_default_check_button, self._type_check_button,
|
||||
self._sid_auto_check_button, self._tid_check_button, self._nid_check_button,
|
||||
self._namespace_check_button)
|
||||
@@ -590,6 +590,10 @@ class IptvListDialog:
|
||||
for el in self._default_elems:
|
||||
el.set_active(True)
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type=Gtk.MessageType.INFO):
|
||||
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
@@ -630,7 +634,7 @@ class IptvListConfigurationDialog(IptvListDialog):
|
||||
@run_idle
|
||||
def on_apply(self, item):
|
||||
if not is_data_correct(self._digit_elems):
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
self.show_info_message("Error. Verify the data!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
@@ -677,7 +681,7 @@ class IptvListConfigurationDialog(IptvListDialog):
|
||||
self._bouquet.clear()
|
||||
list(map(lambda r: self._bouquet.append(r[Column.FAV_ID]), self._fav_model))
|
||||
|
||||
self._info_bar.set_visible(True)
|
||||
self.show_info_message("Done!", Gtk.MessageType.INFO)
|
||||
self._ok_button.set_visible(True)
|
||||
|
||||
|
||||
@@ -691,6 +695,7 @@ class M3uImportDialog(IptvListDialog):
|
||||
self._picons = app.picons
|
||||
self._pic_path = app._settings.profile_picons_path
|
||||
self._services = None
|
||||
self._epg_src = None
|
||||
self._url_count = 0
|
||||
self._errors_count = 0
|
||||
self._max_count = 0
|
||||
@@ -699,57 +704,74 @@ class M3uImportDialog(IptvListDialog):
|
||||
self._dialog.set_title(translate("Playlist import"))
|
||||
self._dialog.connect("delete-event", self.on_close)
|
||||
self._apply_button.set_label(translate("Import"))
|
||||
# Progress
|
||||
self._progress_bar = Gtk.ProgressBar(visible=False, valign="center")
|
||||
self._spinner = Gtk.Spinner(active=False)
|
||||
self._info_label = Gtk.Label(visible=True, ellipsize="end", max_width_chars=30)
|
||||
load_label = Gtk.Label(label=translate("Loading data..."))
|
||||
self._spinner.bind_property("active", self._spinner, "visible")
|
||||
self._spinner.bind_property("visible", load_label, "visible")
|
||||
# Extra box.
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}m3u.glade", use_str=True, objects=("import_m3u_box",))
|
||||
self._info_label = builder.get_object("info_label")
|
||||
self._progress_bar = builder.get_object("progress_bar")
|
||||
self._spinner = builder.get_object("spinner")
|
||||
self._spinner.bind_property("active", self._start_values_grid, "sensitive", 4)
|
||||
self._picon_switch = builder.get_object("picon_switch")
|
||||
self._picon_box = builder.get_object("picon_box")
|
||||
# Type import buttons.
|
||||
self._current_bq_button = builder.get_object("current_bq_button")
|
||||
self._single_bq_button = builder.get_object("single_bq_button")
|
||||
self._group_bq_button = builder.get_object("group_bq_button")
|
||||
self._sub_bq_button = builder.get_object("sub_bq_button")
|
||||
# EPG src.
|
||||
self._epg_links_button = builder.get_object("epg_links_box")
|
||||
self._add_epg_src_switch = builder.get_object("add_epg_src_switch")
|
||||
|
||||
progress_box = Gtk.HBox(visible=True, spacing=2)
|
||||
progress_box.add(self._progress_bar)
|
||||
progress_box.pack_end(self._spinner, False, False, 0)
|
||||
progress_box.pack_start(load_label, False, False, 0)
|
||||
# Picons
|
||||
self._picons_switch = Gtk.Switch(visible=True)
|
||||
self._picon_box = Gtk.HBox(visible=True, sensitive=False, spacing=5)
|
||||
self._picon_box.pack_end(self._picons_switch, False, False, 0)
|
||||
self._picon_box.pack_end(Gtk.Label(visible=True, label=translate("Download picons")), False, False, 0)
|
||||
# Extra box
|
||||
extra_box = Gtk.HBox(visible=True, spacing=2, margin_bottom=5, margin_top=5)
|
||||
extra_box.set_center_widget(progress_box)
|
||||
extra_box.pack_start(self._info_label, False, False, 5)
|
||||
extra_box.pack_end(self._picon_box, True, True, 5)
|
||||
|
||||
frame = Gtk.Frame(visible=True, margin_bottom=5)
|
||||
frame.add(extra_box)
|
||||
self._data_box.add(frame)
|
||||
m3u_box = builder.get_object("import_m3u_box")
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
self._data_box.add(m3u_box)
|
||||
else:
|
||||
self._data_box.set_visible(False)
|
||||
self._group_bq_button.set_sensitive(False)
|
||||
self._sub_bq_button.set_sensitive(False)
|
||||
m3u_box.set_margin_start(5)
|
||||
m3u_box.set_margin_end(5)
|
||||
area = self._dialog.get_content_area()
|
||||
area.pack_start(m3u_box, True, True, 0)
|
||||
area.reorder_child(m3u_box, 0)
|
||||
|
||||
self.get_m3u(m3_path, s_type)
|
||||
|
||||
@run_task
|
||||
def get_m3u(self, path, s_type):
|
||||
try:
|
||||
GLib.idle_add(self._spinner.set_property, "active", True)
|
||||
self._services = parse_m3u(path, s_type)
|
||||
GLib.idle_add(self._spinner.start)
|
||||
self._epg_src, self._services = parse_m3u(path, s_type)
|
||||
for s in self._services:
|
||||
if s.picon:
|
||||
GLib.idle_add(self._picon_box.set_sensitive, True)
|
||||
break
|
||||
finally:
|
||||
msg = f"{translate('Streams detected:')} {len(self._services) if self._services else 0}."
|
||||
GLib.idle_add(self._info_label.set_text, msg)
|
||||
GLib.idle_add(self._spinner.set_property, "active", False)
|
||||
self.update_info()
|
||||
|
||||
@run_idle
|
||||
def update_info(self):
|
||||
msg = f"{translate('Streams detected:')} {len(self._services) if self._services else 0}."
|
||||
self._info_label.set_text(msg)
|
||||
self._spinner.stop()
|
||||
|
||||
if self._epg_src:
|
||||
self._epg_links_button.set_visible(True)
|
||||
[self._epg_links_button.append(u, u) for u in self._epg_src]
|
||||
self._epg_links_button.set_active(0)
|
||||
|
||||
def on_apply(self, item):
|
||||
if self._current_bq_button.get_active() and not self._app.current_bouquet:
|
||||
self.show_info_message("Error. No bouquet is selected!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
if not is_data_correct(self._digit_elems):
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
self.show_info_message("Error. Verify the data!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
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 = []
|
||||
@@ -775,18 +797,76 @@ class M3uImportDialog(IptvListDialog):
|
||||
|
||||
services.append(s._replace(picon=None, picon_id=picon_id, data_id=None, fav_id=fav_id))
|
||||
|
||||
if self._picons_switch.get_active():
|
||||
if self._add_epg_src_switch.get_active():
|
||||
self.on_add_epg_source()
|
||||
|
||||
if self._picon_switch.get_active():
|
||||
if self.is_default_values():
|
||||
show_dialog(DialogType.ERROR, self._dialog,
|
||||
"Set values for TID, NID and Namespace for correct naming of the picons!")
|
||||
msg = "Set values for TID, NID and Namespace for correct naming of the picons!"
|
||||
self.show_info_message(msg, Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
self.download_picons(picons)
|
||||
else:
|
||||
GLib.idle_add(self._ok_button.set_visible, True)
|
||||
GLib.idle_add(self._info_bar.set_visible, True, priority=GLib.PRIORITY_LOW)
|
||||
self.on_apply_done()
|
||||
|
||||
self._app.append_imported_services(services)
|
||||
self.import_services(services)
|
||||
|
||||
def import_services(self, services):
|
||||
if self._current_bq_button.get_active():
|
||||
self._app.append_imported_services(services)
|
||||
return
|
||||
|
||||
s_type = self._app.app_settings.setting_type
|
||||
model = self._app.bouquets_view.get_model()
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
itr = model.get_iter_first()
|
||||
else:
|
||||
# We will use the 'FAV' section for Neutrino!
|
||||
itr = model.get_iter(Gtk.TreePath.new_from_indices([1]))
|
||||
|
||||
bqs = self._app.current_bouquets
|
||||
bq_type = model.get_value(itr, Column.BQ_TYPE)
|
||||
def_bq_name = gen_bouquet_name(bqs, f"IPTV {date.today()} ", bq_type)
|
||||
|
||||
if self._single_bq_button.get_active():
|
||||
self.append_bouquet(def_bq_name, bq_type, bqs, model, itr, services)
|
||||
else:
|
||||
# Sub-bouquets.
|
||||
if self._sub_bq_button.get_active():
|
||||
itr = self.append_bouquet(gen_bouquet_name(bqs, def_bq_name, bq_type), bq_type, bqs, model, itr, ())
|
||||
# Generating groups with skipping markers.
|
||||
m_name = BqServiceType.MARKER.value
|
||||
def_bq_name = f"{def_bq_name} [No group]"
|
||||
gr = self.get_services_groups(filter(lambda s: s.service_type != m_name, services), def_bq_name)
|
||||
[self.append_bouquet(gen_bouquet_name(bqs, g, bq_type), bq_type, bqs, model, itr, s) for g, s in gr.items()]
|
||||
|
||||
def append_bouquet(self, bq_name, bq_type, bqs, model, itr, services):
|
||||
""" Adds new bouquet and returns iter of appended row. """
|
||||
cur_services = self._app.current_services
|
||||
bqs[f"{bq_name}:{bq_type}"] = [s.fav_id for s in services]
|
||||
cur_services.update({s.fav_id: s for s in services})
|
||||
bq = (bq_name, None, None, bq_type)
|
||||
return model.append(itr, bq)
|
||||
|
||||
def get_services_groups(self, services, def_gr_name="No group"):
|
||||
def grouper(s):
|
||||
return s.package or def_gr_name
|
||||
|
||||
return {k: list(v) for k, v in groupby(sorted(services, key=grouper), key=grouper)}
|
||||
|
||||
def on_add_epg_source(self):
|
||||
active_src = self._epg_links_button.get_active_id()
|
||||
settings = self._app.app_settings
|
||||
sources = settings.epg_xml_sources
|
||||
log(f"Adding an EPG source -> {active_src}")
|
||||
if active_src not in set(sources):
|
||||
sources.append(active_src)
|
||||
settings.epg_xml_sources = sources
|
||||
self._app.emit("epg-settings-changed", None)
|
||||
else:
|
||||
log(f"{translate('This URL already exists!')}")
|
||||
|
||||
@run_task
|
||||
def download_picons(self, picons):
|
||||
@@ -868,10 +948,15 @@ class M3uImportDialog(IptvListDialog):
|
||||
model.set_value(r.iter, Column.FAV_PICON, picons.get(s.picon_id, None))
|
||||
yield True
|
||||
|
||||
self._info_bar.set_visible(True)
|
||||
self._ok_button.set_visible(True)
|
||||
self.on_apply_done()
|
||||
yield True
|
||||
|
||||
@run_idle
|
||||
def on_apply_done(self):
|
||||
self.show_info_message("Done!", Gtk.MessageType.INFO)
|
||||
self._ok_button.set_visible(True)
|
||||
self._picon_box.set_sensitive(False)
|
||||
|
||||
def on_response(self, dialog, response):
|
||||
if response == Gtk.ResponseType.APPLY:
|
||||
return True
|
||||
@@ -890,6 +975,134 @@ class M3uImportDialog(IptvListDialog):
|
||||
return False
|
||||
|
||||
|
||||
class ExportM3uDialog(BaseDialog):
|
||||
def __init__(self, app, bouquets):
|
||||
super().__init__(app.app_window, "Export to m3u",
|
||||
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, translate("Save"), Gtk.ResponseType.OK))
|
||||
self._app = app
|
||||
self._bouquets = bouquets
|
||||
self._url = None
|
||||
self._default_port = "8001"
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}m3u.glade", use_str=True, objects=("export_m3u_box",))
|
||||
self._main_grid = builder.get_object("export_m3u_grid")
|
||||
self._port_entry = builder.get_object("export_port_entry")
|
||||
self._port_auto_button = builder.get_object("export_auto_button")
|
||||
self._all_type_button = builder.get_object("export_all_button")
|
||||
self._iptv_type_button = builder.get_object("export_iptv_button")
|
||||
self._grp_bq_button = builder.get_object("export_grp_bq_button")
|
||||
self._grp_marker_button = builder.get_object("export_grp_markers_button")
|
||||
self._bq_count_label = builder.get_object("export_bq_count_label")
|
||||
self._services_count_label = builder.get_object("export_services_count_label")
|
||||
self.get_content_area().pack_start(builder.get_object("export_m3u_box"), False, False, 0)
|
||||
|
||||
is_enigma = self._app.is_enigma
|
||||
self._port_auto_button.set_active(True) if is_enigma else self._main_grid.remove_row(0)
|
||||
self._grp_marker_button.set_visible(is_enigma)
|
||||
self._all_type_button.set_active(True) if is_enigma else self._iptv_type_button.set_active(True)
|
||||
self._all_type_button.set_sensitive(is_enigma)
|
||||
|
||||
self.connect("response", self.on_response)
|
||||
self.connect("realize", self.init)
|
||||
|
||||
def init(self, widget=None):
|
||||
self._bq_count_label.set_text(str(len(self._bouquets)))
|
||||
self._services_count_label.set_text(str(len(list(chain.from_iterable(self._bouquets.values())))))
|
||||
|
||||
if self._app.is_enigma:
|
||||
self._port_entry.connect("changed", self.on_port_changed)
|
||||
self._port_auto_button.connect("toggled", self.on_port_auto_toggled)
|
||||
# Add style for the port entry.
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(_CSS_PATH)
|
||||
context = self._port_entry.get_style_context()
|
||||
context.add_provider_for_screen(Gdk.Screen.get_default(), style_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
def on_port_changed(self, entry):
|
||||
entry.set_name(_DIGIT_ENTRY_NAME if _PATTERN.search(entry.get_text()) else "GtkEntry")
|
||||
|
||||
def on_port_auto_toggled(self, button):
|
||||
if not button.get_active() and not self._port_entry.get_text():
|
||||
self._port_entry.set_text(self._default_port)
|
||||
|
||||
def on_response(self, dialog, response):
|
||||
if response != Gtk.ResponseType.OK:
|
||||
self.destroy()
|
||||
else:
|
||||
if self._app.is_enigma:
|
||||
if self._port_auto_button.get_active():
|
||||
self.do_export_auto()
|
||||
else:
|
||||
if self._port_entry.get_name() == _DIGIT_ENTRY_NAME:
|
||||
self._app.show_error_message("Error. Verify the data!")
|
||||
else:
|
||||
st = self._app.app_settings
|
||||
self._url = f"http{'s' if st.http_use_ssl else ''}://{st.host}:{self._port_entry.get_text()}/"
|
||||
self.do_export()
|
||||
else:
|
||||
self.do_export()
|
||||
return True
|
||||
|
||||
def do_export_auto(self, button=None):
|
||||
""" Retrieves streaming port from Receiver via HTTP API and starts export.
|
||||
|
||||
Since the streaming port can be changed by the user,
|
||||
we're getting base link to the stream -> http(s)://IP:PORT/
|
||||
"""
|
||||
from app.connections import HttpAPI
|
||||
|
||||
sent = self._app.send_http_request(HttpAPI.Request.STREAM, "", self.start_export)
|
||||
self._port_auto_button.set_active(sent)
|
||||
self._port_auto_button.set_sensitive(sent)
|
||||
|
||||
def start_export(self, data):
|
||||
self._port_auto_button.set_active("error_code" not in data)
|
||||
|
||||
url = self._app.get_url_from_m3u(data)
|
||||
url = urlparse(url)
|
||||
if all((url.scheme, url.port)):
|
||||
self._url = url.geturl()
|
||||
self._port_entry.set_text(str(url.port))
|
||||
self.do_export()
|
||||
|
||||
@run_idle
|
||||
def do_export(self):
|
||||
self.destroy()
|
||||
|
||||
services = self._app.current_services
|
||||
|
||||
def get_service(fav_id, num=0):
|
||||
srv = services.get(fav_id, None)
|
||||
if srv:
|
||||
s_type = BqServiceType(srv.service_type)
|
||||
if s_type is BqServiceType.DEFAULT:
|
||||
srv = services.get(fav_id, None)
|
||||
s_data = srv.picon_id.rstrip(".png").replace("_", ":") if srv.picon_id else None
|
||||
return BouquetService(srv.service, s_type, s_data, num)
|
||||
return BouquetService(srv.service, s_type, fav_id, num)
|
||||
return BouquetService("N/A", BqServiceType.MARKER, fav_id, num)
|
||||
|
||||
# Preparing bouquets data.
|
||||
bouquets = {b[:b.rindex(":")]: [get_service(i) for i in s] for b, s in self._bouquets.items()}
|
||||
|
||||
bq_services = []
|
||||
s_types = {BqServiceType.IPTV}
|
||||
if self._all_type_button.get_active():
|
||||
s_types.add(BqServiceType.DEFAULT)
|
||||
|
||||
if self._grp_bq_button.get_active():
|
||||
for b, bs in bouquets.items():
|
||||
bq_services.append(BouquetService(b, BqServiceType.MARKER, None, 0))
|
||||
bq_services.extend(filter(lambda s: s.type in s_types, bs))
|
||||
elif self._grp_marker_button.get_active():
|
||||
bq_services = chain.from_iterable(bouquets.values())
|
||||
else:
|
||||
bq_services = filter(lambda s: s.type in s_types, chain.from_iterable(bouquets.values()))
|
||||
|
||||
file_name = f"{'_'.join(list(bouquets)[:10])}__{date.today().strftime('%Y_%m_%d')}"
|
||||
self._app.save_bouquet_to_m3u(bq_services, self._url, file_name)
|
||||
|
||||
|
||||
class YtListImportDialog:
|
||||
def __init__(self, app):
|
||||
handlers = {"on_import": self.on_import,
|
||||
@@ -955,7 +1168,7 @@ class YtListImportDialog:
|
||||
self._dialog.resize(*window_size)
|
||||
# Style.
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(f"{UI_RESOURCES_PATH}style.css")
|
||||
style_provider.load_from_path(_CSS_PATH)
|
||||
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
@@ -1072,7 +1285,7 @@ class YtListImportDialog:
|
||||
srvs.append(srv)
|
||||
|
||||
self.appender(srvs)
|
||||
self.show_info_message(translate("Done!"), Gtk.MessageType.INFO)
|
||||
self.show_info_message("Done!", Gtk.MessageType.INFO)
|
||||
|
||||
@run_idle
|
||||
def update_active_elements(self, sensitive):
|
||||
@@ -1103,9 +1316,7 @@ class YtListImportDialog:
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
|
||||
|
||||
def on_selected_toggled(self, toggle, path):
|
||||
self._model.set_value(self._model.get_iter(path), 2, not toggle.get_active())
|
||||
@@ -1120,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.
783
app/ui/m3u.glade
Normal file
783
app/ui/m3u.glade
Normal file
@@ -0,0 +1,783 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.40.0
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.22"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor. -->
|
||||
<!-- interface-copyright 2018-2024 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkBox" id="export_m3u_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkViewport" id="export_viewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<!-- n-columns=2 n-rows=3 -->
|
||||
<object class="GtkGrid" id="export_m3u_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</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="row-spacing">5</property>
|
||||
<property name="column-spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="export_port_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Port:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="export_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Export:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="export_types_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="export_all_button">
|
||||
<property name="label" translatable="yes">All types</property>
|
||||
<property name="name">header-button</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>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="export_iptv_button">
|
||||
<property name="label" translatable="yes">IPTV only</property>
|
||||
<property name="name">header-button</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>
|
||||
<property name="group">export_all_button</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">1</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="export_grp_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="export_grp_bq_button">
|
||||
<property name="label" translatable="yes">Bouquets</property>
|
||||
<property name="name">header-button</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>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="export_grp_markers_button">
|
||||
<property name="label" translatable="yes">Markers</property>
|
||||
<property name="name">header-button</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>
|
||||
<property name="group">export_grp_bq_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="export_grp_no_button">
|
||||
<property name="label" translatable="yes">No</property>
|
||||
<property name="name">header-button</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>
|
||||
<property name="group">export_grp_markers_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="left-attach">1</property>
|
||||
<property name="top-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="export_port_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive" bind-source="export_iptv_button" bind-property="active" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="export_port_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive" bind-source="export_auto_button" bind-property="active" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="width-chars">10</property>
|
||||
<property name="primary-icon-name">document-edit-symbolic</property>
|
||||
<property name="placeholder-text" translatable="yes">8001</property>
|
||||
<property name="input-purpose">digits</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="export_auto_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">emblem-synchronizing-symbolic</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" translatable="yes">Auto</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</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">1</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="export_group_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Group by</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">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="export_status_box">
|
||||
<property name="height-request">26</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="export_info_image">
|
||||
<property name="visible">True</property>
|
||||
<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="icon-name">document-properties</property>
|
||||
<property name="icon_size">2</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="export_bq_info_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="export_bq_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Bouquets</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>
|
||||
<child>
|
||||
<object class="GtkLabel" id="export_bq_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="label">0</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">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="export_service_info_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="spacing">1</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="export_services_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Services</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>
|
||||
<child>
|
||||
<object class="GtkLabel" id="export_services_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="label">0</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">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</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>
|
||||
<object class="GtkBox" id="import_m3u_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkFrame" id="import_m3u_frame">
|
||||
<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="label-xalign">0.02</property>
|
||||
<property name="shadow-type">none</property>
|
||||
<child>
|
||||
<object class="GtkViewport" id="import_viewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="import_main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</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">10</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="import_type_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="current_bq_button">
|
||||
<property name="label" translatable="yes">Current bouquet</property>
|
||||
<property name="name">header-button</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>
|
||||
<property name="group">sub_bq_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="single_bq_button">
|
||||
<property name="label" translatable="yes">Single bouquet</property>
|
||||
<property name="name">header-button</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>
|
||||
<property name="group">current_bq_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="group_bq_button">
|
||||
<property name="label" translatable="yes">Split by groups</property>
|
||||
<property name="name">header-button</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>
|
||||
<property name="group">sub_bq_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="sub_bq_button">
|
||||
<property name="label" translatable="yes">Create sub-bouquets</property>
|
||||
<property name="name">header-button</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>
|
||||
<property name="group">current_bq_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</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="GtkBox" id="load_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">15</property>
|
||||
<child type="center">
|
||||
<object class="GtkProgressBar" id="progress_bar">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="info_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="info_label">
|
||||
<property name="visible" bind-source="spinner" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="max-width-chars">30</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="load_label">
|
||||
<property name="visible" bind-source="spinner" bind-property="visible">False</property>
|
||||
<property name="can-focus">False</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>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="spinner">
|
||||
<property name="visible" bind-source="spinner" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</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="picon_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="picon_switch_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Download picons</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="picon_switch">
|
||||
<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">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_src_box">
|
||||
<property name="height-request">30</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="epg_source_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">EPG source</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>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="epg_links_box">
|
||||
<property name="can-focus">True</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="active">0</property>
|
||||
<property name="has-entry">True</property>
|
||||
<child internal-child="entry">
|
||||
<object class="GtkEntry">
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="epg_info_label">
|
||||
<property name="visible" bind-source="epg_links_box" bind-property="visible" bind-flags="invert-boolean">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="label" translatable="yes">Not found.</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="add_epg_src_box">
|
||||
<property name="visible" bind-source="epg_links_box" bind-property="visible">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="add_epg_src_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Add to EPG sources list</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="add_epg_src_switch">
|
||||
<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">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="view"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="import_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-bottom">2</property>
|
||||
<property name="label" translatable="yes">Import</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
File diff suppressed because it is too large
Load Diff
812
app/ui/main.py
812
app/ui/main.py
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -33,7 +33,8 @@ __all__ = ("insert_marker", "move_items", "rename", "ViewTarget", "set_flags", "
|
||||
"is_only_one_item_selected", "gen_bouquets", "BqGenType", "get_selection", "get_service_reference",
|
||||
"get_model_data", "remove_all_unused_picons", "get_picon_pixbuf", "get_base_itrs", "get_iptv_url",
|
||||
"get_iptv_data", "update_entry_data", "append_text_to_tview", "on_popup_menu", "get_picon_file_name",
|
||||
"update_toggle_model", "update_popup_filter_model", "update_filter_sat_positions", "get_pos_num")
|
||||
"update_toggle_model", "update_popup_filter_model", "update_filter_sat_positions", "get_pos_num",
|
||||
"show_info_bar_message", "gen_bouquet_name")
|
||||
|
||||
import os
|
||||
import re
|
||||
@@ -48,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
|
||||
@@ -293,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)
|
||||
@@ -685,6 +686,19 @@ def get_bouquets_names(model):
|
||||
return bouquets_names
|
||||
|
||||
|
||||
def gen_bouquet_name(bouquets, base_name, bq_type):
|
||||
""" Generates a name for new bouquets. """
|
||||
count = 0
|
||||
key = f"{base_name}:{bq_type}"
|
||||
bq_name = base_name
|
||||
while key in bouquets:
|
||||
count += 1
|
||||
bq_name = f"{base_name}{count}"
|
||||
key = f"{bq_name}:{bq_type}"
|
||||
|
||||
return bq_name
|
||||
|
||||
|
||||
def get_services_type_groups(services):
|
||||
""" Returns services grouped by main types [TV, Radio, Data]. -> dict """
|
||||
|
||||
@@ -805,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
|
||||
|
||||
@@ -819,13 +836,16 @@ def append_text_to_tview(char, view):
|
||||
|
||||
|
||||
def get_iptv_url(row, s_type, column=Column.FAV_ID):
|
||||
""" Returns url from iptv type row """
|
||||
""" Returns URL from IPTV type row. """
|
||||
data = row[column].split(":" if s_type is SettingsType.ENIGMA_2 else "::")
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
data = list(filter(lambda x: "http" in x, data))
|
||||
|
||||
if data:
|
||||
url = data[0]
|
||||
return unquote(url) if s_type is SettingsType.ENIGMA_2 else url
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
if len(data) > 10 and "http" in data[10]:
|
||||
url, sep, desc = data[10].partition("#DESCRIPTION")
|
||||
return unquote(url.strip())
|
||||
else:
|
||||
return data[0]
|
||||
|
||||
|
||||
def get_iptv_data(fav_id):
|
||||
@@ -838,10 +858,27 @@ 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)
|
||||
|
||||
|
||||
def show_info_bar_message(bar, label, text, message_type=Gtk.MessageType.INFO):
|
||||
""" Shows a message for info bars. """
|
||||
bar.set_visible(False)
|
||||
label.set_text(translate(text))
|
||||
bar.set_message_type(message_type)
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -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-2024 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkMenu" id="add_menu">
|
||||
<property name="visible">True</property>
|
||||
@@ -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>
|
||||
@@ -428,8 +429,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">list-add</property>
|
||||
<property name="icon_size">0</property>
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@@ -441,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>
|
||||
@@ -451,7 +452,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkImage" id="receive_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="stock">gtk-goto-bottom</property>
|
||||
<property name="icon-name">go-bottom-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@@ -463,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>
|
||||
@@ -491,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>
|
||||
@@ -499,7 +500,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">window-new</property>
|
||||
<property name="icon-name">window-new-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@@ -530,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>
|
||||
@@ -592,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>
|
||||
@@ -608,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>
|
||||
@@ -742,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>
|
||||
@@ -850,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>
|
||||
@@ -869,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>
|
||||
@@ -953,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>
|
||||
@@ -1027,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>
|
||||
@@ -1040,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>
|
||||
@@ -1056,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>
|
||||
@@ -1073,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>
|
||||
@@ -1104,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>
|
||||
@@ -1572,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>
|
||||
@@ -1689,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">
|
||||
@@ -1808,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>
|
||||
@@ -1823,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>
|
||||
@@ -1837,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-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
|
||||
@@ -30,6 +30,7 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
from enum import Enum
|
||||
from html import escape
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse, unquote
|
||||
|
||||
@@ -39,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
|
||||
|
||||
|
||||
@@ -56,6 +57,7 @@ class PiconManager(Gtk.Box):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("data-open", self.on_open)
|
||||
self._app.connect("data-receive", self.on_download)
|
||||
self._app.connect("data-send", self.on_send)
|
||||
self._app.connect("page-changed", self.update_picons_dest)
|
||||
@@ -151,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")
|
||||
@@ -167,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.
|
||||
@@ -192,6 +177,8 @@ class PiconManager(Gtk.Box):
|
||||
column.set_cell_data_func(builder.get_object("picons_dest_renderer"), self.picon_data_func)
|
||||
column = builder.get_object("src_picon_column")
|
||||
column.set_cell_data_func(builder.get_object("picons_src_renderer"), self.picon_data_func)
|
||||
column = builder.get_object("dest_title_column")
|
||||
column.set_cell_data_func(builder.get_object("title_dest_renderer"), self.title_data_func)
|
||||
# Settings
|
||||
self._settings = settings
|
||||
self._s_type = settings.setting_type
|
||||
@@ -219,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)
|
||||
|
||||
@@ -228,8 +217,11 @@ class PiconManager(Gtk.Box):
|
||||
if is_explorer:
|
||||
self.update_picons_data(self._picons_dest_view)
|
||||
|
||||
def on_open(self):
|
||||
def on_open(self, app, page):
|
||||
""" Opens picons from local path [in src view]. """
|
||||
if page is not Page.PICONS:
|
||||
return
|
||||
|
||||
response = show_dialog(DialogType.CHOOSER, self._app.app_window, settings=self._settings, title="Open folder")
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
@@ -245,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:
|
||||
@@ -293,6 +286,17 @@ class PiconManager(Gtk.Box):
|
||||
def picon_data_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("pixbuf", get_pixbuf_at_scale(model.get_value(itr, 2), 72, 48, True))
|
||||
|
||||
def title_data_func(self, column, renderer, model, itr, data):
|
||||
srv = self._services.get(model[itr][1], None)
|
||||
if srv:
|
||||
renderer.set_property("markup", self.get_picon_info_markup(srv))
|
||||
|
||||
def get_picon_info_markup(self, srv):
|
||||
ext_info = "" if srv.service_type == "IPTV" else f" {srv.pos} {srv.freq}"
|
||||
return (f'{escape(srv.picon_id)}\n\n'
|
||||
f'<span size="small" weight="bold">{translate("Service")}: {escape(srv.service)}</span>\n'
|
||||
f'<span size="small" style="italic">{srv.service_type}{ext_info}</span>')
|
||||
|
||||
def update_picons_from_file(self, view, uri):
|
||||
""" Adds picons in the view on dragging from file system. """
|
||||
path = Path(urlparse(unquote(uri)).path.strip())
|
||||
@@ -662,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:
|
||||
@@ -807,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):
|
||||
@@ -971,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)
|
||||
|
||||
@@ -1000,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
|
||||
@@ -1021,12 +1041,7 @@ class PiconManager(Gtk.Box):
|
||||
show_dialog(dialog_type, self._app_window, message)
|
||||
|
||||
def get_picons_format(self):
|
||||
picon_format = SettingsType.ENIGMA_2
|
||||
|
||||
if self._neutrino_mp_radio_button.get_active():
|
||||
picon_format = SettingsType.NEUTRINO_MP
|
||||
|
||||
return picon_format
|
||||
return SettingsType.NEUTRINO_MP if self._neutrino_mp_radio_button.get_active() else SettingsType.ENIGMA_2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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-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
|
||||
@@ -46,15 +46,16 @@ class RecordingsTool(Gtk.Box):
|
||||
ROOT = ".."
|
||||
DEFAULT_PATH = "/hdd"
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def __init__(self, app, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("layout-changed", self.on_layout_changed)
|
||||
self._app.connect("data-receive", self.on_data_receive)
|
||||
self._app.connect("profile-changed", self.init)
|
||||
self._app.connect("filter-toggled", self.on_filter_toggled)
|
||||
|
||||
self._settings = settings
|
||||
self._settings = app.app_settings
|
||||
self._ftp = None
|
||||
self._logos = {}
|
||||
# Icon.
|
||||
@@ -82,6 +83,7 @@ class RecordingsTool(Gtk.Box):
|
||||
self._filter_model = builder.get_object("recordings_filter_model")
|
||||
self._filter_model.set_visible_func(self.recordings_filter_function)
|
||||
self._filter_entry = builder.get_object("recordings_filter_entry")
|
||||
self._recordings_filter_button = builder.get_object("recordings_filter_button")
|
||||
self._recordings_count_label = builder.get_object("recordings_count_label")
|
||||
self.pack_start(builder.get_object("recordings_box"), True, True, 0)
|
||||
self._rec_view.get_model().set_sort_func(3, self.time_sort_func, 3)
|
||||
@@ -92,7 +94,7 @@ class RecordingsTool(Gtk.Box):
|
||||
renderer.set_fixed_size(size, size * 0.65)
|
||||
srv_column.set_cell_data_func(renderer, self.logo_data_func)
|
||||
|
||||
if settings.alternate_layout:
|
||||
if self._settings.alternate_layout:
|
||||
self.on_layout_changed(app, True)
|
||||
|
||||
self.init()
|
||||
@@ -141,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
|
||||
@@ -293,16 +296,18 @@ class RecordingsTool(Gtk.Box):
|
||||
txt = self._filter_entry.get_text().upper()
|
||||
return next((s for s in model.get(itr, 1, 2, 3, 5, 6) if s and txt in s.upper()), False)
|
||||
|
||||
def on_filter_toggled(self, app, value):
|
||||
if self._app.page is Page.RECORDINGS:
|
||||
self._recordings_filter_button.set_active(not self._recordings_filter_button.get_active())
|
||||
|
||||
def on_recordings_filter_toggled(self, button):
|
||||
if not button.get_active():
|
||||
self._filter_entry.set_text("")
|
||||
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">
|
||||
@@ -1050,6 +1052,7 @@ Author: Dmitriy Yefremov
|
||||
<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="label-xalign">0.019999999552965164</property>
|
||||
<property name="shadow-type">none</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
|
||||
@@ -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]{4})(,c:[0-9]{2}[0-9a-fA-F]{4})*")
|
||||
# Buttons
|
||||
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.
|
||||
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-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 app.commons import run_task, run_idle, log
|
||||
from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException
|
||||
from app.settings import SettingsType, Settings, PlayStreamsMode, PlaybackMode, IS_LINUX, SEP, IS_WIN
|
||||
from app.ui.dialogs import show_dialog, DialogType, translate, get_chooser_dialog, get_builder
|
||||
from .main_helper import update_entry_data, scroll_to, get_picon_pixbuf
|
||||
from .main_helper import update_entry_data, scroll_to, get_picon_pixbuf, show_info_bar_message
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, DEFAULT_ICON, APP_FONT, HeaderBar
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ class SettingsDialog:
|
||||
"on_force_bq_name": self.on_force_bq_name,
|
||||
"on_http_mode_switch": self.on_http_mode_switch,
|
||||
"on_experimental_switch": self.on_experimental_switch,
|
||||
"on_yt_dl_switch": self.on_yt_dl_switch,
|
||||
"on_default_path_mode_switch": self.on_default_path_mode_switch,
|
||||
"on_profile_add": self.on_profile_add,
|
||||
"on_profile_edit": self.on_profile_edit,
|
||||
@@ -186,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")
|
||||
@@ -269,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)
|
||||
@@ -291,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)
|
||||
@@ -339,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)
|
||||
@@ -352,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()
|
||||
@@ -421,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()
|
||||
@@ -431,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()
|
||||
@@ -441,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),
|
||||
@@ -455,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)
|
||||
@@ -465,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)
|
||||
@@ -476,10 +494,7 @@ class SettingsDialog:
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(False)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(translate(text))
|
||||
self._info_bar.set_visible(True)
|
||||
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
|
||||
|
||||
@run_idle
|
||||
def show_spinner(self, show):
|
||||
@@ -501,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):
|
||||
@@ -513,9 +529,6 @@ class SettingsDialog:
|
||||
else:
|
||||
self.on_info_bar_close()
|
||||
|
||||
def on_yt_dl_switch(self, switch, state):
|
||||
self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING)
|
||||
|
||||
def on_default_path_mode_switch(self, switch, state):
|
||||
self._use_common_picon_path_switch.set_active(False) if state else None
|
||||
|
||||
@@ -679,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()
|
||||
@@ -236,15 +228,15 @@ class TimerTool(Gtk.Box):
|
||||
TimerTool.set_repetition_flags(int(self._timer_data.get("e2repeated", "0")), self._days_buttons)
|
||||
|
||||
def set_timer_from_event_data(self):
|
||||
self._timer_name_entry.set_text(self._timer_data.get("e2eventtitle", ""))
|
||||
self._timer_desc_entry.set_text(self._timer_data.get("e2eventdescription", ""))
|
||||
self._timer_service_entry.set_text(self._timer_data.get("e2eventservicename", ""))
|
||||
self._timer_service_ref_entry.set_text(self._timer_data.get("e2eventservicereference", ""))
|
||||
self._timer_event_id_entry.set_text(self._timer_data.get("e2eventid", ""))
|
||||
self._timer_name_entry.set_text(self._timer_data.get("e2eventtitle", None) or "")
|
||||
self._timer_desc_entry.set_text(self._timer_data.get("e2eventdescription", None) or "")
|
||||
self._timer_service_entry.set_text(self._timer_data.get("e2eventservicename", None) or "")
|
||||
self._timer_service_ref_entry.set_text(self._timer_data.get("e2eventservicereference", None) or "")
|
||||
self._timer_event_id_entry.set_text(self._timer_data.get("e2eventid", None) or "")
|
||||
self._timer_action_combo_box.set_active_id("1")
|
||||
self._timer_after_combo_box.set_active_id("3")
|
||||
start_time = int(self._timer_data.get("e2eventstart", "0"))
|
||||
self.set_time_data(start_time, start_time + int(self._timer_data.get("e2eventduration", "0")))
|
||||
start_time = int(self._timer_data.get("e2eventstart", "0") or "0")
|
||||
self.set_time_data(start_time, start_time + int(self._timer_data.get("e2eventduration", "0") or "0"))
|
||||
|
||||
def set_time_data(self, start_time, end_time):
|
||||
""" Sets values for time widgets. """
|
||||
@@ -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">
|
||||
@@ -43,7 +43,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="row-spacing">5</property>
|
||||
<property name="column-spacing">5</property>
|
||||
<property name="column-homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="cable_freq_entry">
|
||||
<property name="visible">True</property>
|
||||
@@ -105,6 +104,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="cable_freq_label">
|
||||
<property name="visible">True</property>
|
||||
@@ -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>
|
||||
@@ -542,7 +542,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="can-focus">False</property>
|
||||
<property name="row-spacing">5</property>
|
||||
<property name="column-spacing">5</property>
|
||||
<property name="column-homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="freq_entry">
|
||||
<property name="visible">True</property>
|
||||
@@ -654,6 +653,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sat_freq_label">
|
||||
<property name="visible">True</property>
|
||||
@@ -879,13 +879,12 @@ 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>
|
||||
<property name="row-spacing">5</property>
|
||||
<property name="column-spacing">5</property>
|
||||
<property name="column-homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="pls_mode_box">
|
||||
<property name="visible">True</property>
|
||||
@@ -912,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>
|
||||
@@ -929,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>
|
||||
@@ -946,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>
|
||||
@@ -959,6 +958,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="tr_pls_mode_label">
|
||||
<property name="visible">True</property>
|
||||
@@ -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>
|
||||
@@ -1119,7 +1171,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="can-focus">False</property>
|
||||
<property name="row-spacing">5</property>
|
||||
<property name="column-spacing">5</property>
|
||||
<property name="column-homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="ter_freq_entry">
|
||||
<property name="visible">True</property>
|
||||
@@ -1198,6 +1249,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_freq_label">
|
||||
<property name="visible">True</property>
|
||||
@@ -1436,7 +1488,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="row-spacing">5</property>
|
||||
<property name="column-spacing">5</property>
|
||||
<property name="column-homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_guard_box">
|
||||
<property name="visible">True</property>
|
||||
@@ -1497,6 +1548,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_guard_label">
|
||||
<property name="visible">True</property>
|
||||
|
||||
@@ -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
|
||||
@@ -43,9 +43,9 @@ from app.eparser.ecommons import (PLS_MODE, get_key_by_value, POLARIZATION, FEC,
|
||||
HIERARCHY, Inversion, C_MODULATION, FEC_DEFAULT, TerTransponder, CableTransponder,
|
||||
Bouquet, BouquetService, BqServiceType, Bouquets, BqType)
|
||||
from app.eparser.satxml import get_pos_str
|
||||
from app.settings import USE_HEADER_BAR, Settings, CONFIG_PATH
|
||||
from app.settings import Settings, CONFIG_PATH
|
||||
from app.tools.satellites import SatellitesParser, SatelliteSource, ServicesParser
|
||||
from ..dialogs import show_dialog, DialogType, translate, get_builder
|
||||
from ..dialogs import show_dialog, BaseDialog, DialogType, translate, get_builder
|
||||
from ..main_helper import append_text_to_tview, get_base_model, on_popup_menu, get_services_type_groups
|
||||
from ..search import SearchProvider
|
||||
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HeaderBar
|
||||
@@ -53,22 +53,11 @@ from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HeaderBar
|
||||
_DIALOGS_UI_PATH = f"{UI_RESOURCES_PATH}xml{os.sep}dialogs.glade"
|
||||
|
||||
|
||||
class DVBDialog(Gtk.Dialog):
|
||||
class DVBDialog(BaseDialog):
|
||||
""" Base dialog class for editing DVB (-> *.xml) data. """
|
||||
|
||||
def __init__(self, parent, title, data=None, *args, **kwargs):
|
||||
super().__init__(transient_for=parent,
|
||||
title=translate(title),
|
||||
modal=True,
|
||||
resizable=False,
|
||||
default_width=240,
|
||||
skip_taskbar_hint=True,
|
||||
skip_pager_hint=True,
|
||||
destroy_with_parent=True,
|
||||
use_header_bar=USE_HEADER_BAR,
|
||||
window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
|
||||
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK),
|
||||
*args, **kwargs)
|
||||
super().__init__(parent=parent, title=title, *args, **kwargs)
|
||||
|
||||
self._viewport = Gtk.Viewport(margin_top=2)
|
||||
self._viewport.get_style_context().add_class("view")
|
||||
@@ -218,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)
|
||||
@@ -241,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(),
|
||||
@@ -252,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()
|
||||
@@ -266,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
|
||||
|
||||
@@ -412,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,
|
||||
@@ -452,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")
|
||||
@@ -460,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.
|
||||
@@ -556,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)
|
||||
@@ -604,20 +596,14 @@ class UpdateDialog:
|
||||
def update_receive_button_state(self, model):
|
||||
self._receive_button.set_sensitive((any(r[4] for r in model)))
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._sat_update_info_bar.set_visible(True)
|
||||
self._sat_update_info_bar.set_message_type(message_type)
|
||||
self._info_bar_message_label.set_text(text)
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
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):
|
||||
@@ -701,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
|
||||
@@ -773,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:
|
||||
@@ -989,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-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
|
||||
@@ -60,6 +60,7 @@ class SatellitesTool(Gtk.Box):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("data-open", self.on_open)
|
||||
self._app.connect("data-save", self.on_save)
|
||||
self._app.connect("data-save-as", self.on_save_as)
|
||||
self._app.connect("data-receive", self.on_download)
|
||||
@@ -238,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()
|
||||
@@ -309,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:
|
||||
@@ -328,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:
|
||||
@@ -522,8 +515,10 @@ class SatellitesTool(Gtk.Box):
|
||||
return self._ter_tr_view
|
||||
return self._cable_tr_view
|
||||
|
||||
@run_idle
|
||||
def on_open(self):
|
||||
def on_open(self, app, page):
|
||||
if page is not Page.SATELLITE:
|
||||
return
|
||||
|
||||
xml_file = "satellites.xml"
|
||||
if self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
xml_file = "terrestrial.xml"
|
||||
@@ -554,29 +549,37 @@ class SatellitesTool(Gtk.Box):
|
||||
@run_idle
|
||||
def on_save(self, app, page):
|
||||
if page is Page.SATELLITE and show_dialog(DialogType.QUESTION, self._app.app_window) == Gtk.ResponseType.OK:
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
write_satellites((Satellite(*r) for r in self._satellite_view.get_model()),
|
||||
f"{self._settings.profile_data_path}satellites.xml")
|
||||
elif self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
write_terrestrial((Terrestrial(*r) for r in self._terrestrial_view.get_model()),
|
||||
f"{self._settings.profile_data_path}terrestrial.xml")
|
||||
else:
|
||||
write_cable((Cable(*r) for r in self._cable_view.get_model()),
|
||||
f"{self._settings.profile_data_path}cables.xml")
|
||||
self.save_data(self._settings.profile_data_path)
|
||||
|
||||
def save_data(self, path):
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
write_satellites((Satellite(*r) for r in self._satellite_view.get_model()), f"{path}satellites.xml")
|
||||
elif self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
write_terrestrial((Terrestrial(*r) for r in self._terrestrial_view.get_model()), f"{path}terrestrial.xml")
|
||||
else:
|
||||
write_cable((Cable(*r) for r in self._cable_view.get_model()), f"{path}cables.xml")
|
||||
|
||||
def on_save_as(self, app, page):
|
||||
self._app.show_error_message("Not implemented yet!")
|
||||
if page is not Page.SATELLITE:
|
||||
return
|
||||
|
||||
buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
|
||||
response = show_dialog(DialogType.CHOOSER, self._app.app_window, settings=self._settings, buttons=buttons)
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
self.save_data(response)
|
||||
|
||||
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.8.0_Alpha"
|
||||
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.8.0-Alpha
|
||||
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-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
|
||||
|
||||
@@ -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.8.0.{BUILD_DATE} Alpha",
|
||||
'NSHumanReadableCopyright': u"Copyright © 2023, 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,22 +47,37 @@ 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,
|
||||
name='DemonEditor',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
contents_directory='.',
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False, icon='icon.ico')
|
||||
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()
|
||||
|
||||
@@ -2,14 +2,29 @@
|
||||
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[sk]=Enigma2 editor balíčkov
|
||||
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[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
|
||||
Exec=bash -c 'cd $(dirname %k) && ./start.py'
|
||||
Terminal=false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2023 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 "Атрыманне піконаў для правайдараў"
|
||||
@@ -1497,3 +1503,114 @@ msgstr "Вер."
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Усталявана"
|
||||
|
||||
msgid "Use common folder for picons"
|
||||
msgstr "Скарыстаць агульную тэчку для піконаў"
|
||||
|
||||
msgid "Activates single folder use for several profiles."
|
||||
msgstr "Актывуе выкарыстанне адзінай тэчкі для некалькіх профіляў."
|
||||
|
||||
msgid "Events"
|
||||
msgstr "Падзеі"
|
||||
|
||||
msgid "Markers"
|
||||
msgstr "Маркеры"
|
||||
|
||||
msgid "IPTV only"
|
||||
msgstr "Толькі IPTV"
|
||||
|
||||
msgid "No"
|
||||
msgstr "Не"
|
||||
|
||||
msgid "Not found."
|
||||
msgstr "Не знойдзена."
|
||||
|
||||
msgid "Current EPG cache contents."
|
||||
msgstr "Змесціва бягучага EPG кэша."
|
||||
|
||||
msgid "Source error!"
|
||||
msgstr "Памылка крыніцы!"
|
||||
|
||||
msgid "The EPG source for the favorites list is not set!"
|
||||
msgstr "Не ўсталявана крыніца EPG для спіса абранага!"
|
||||
|
||||
msgid "Add to EPG sources list"
|
||||
msgstr "Дадаць у спіс крыніц EPG"
|
||||
|
||||
msgid "Current bouquet"
|
||||
msgstr "Абраны букет"
|
||||
|
||||
msgid "Single bouquet"
|
||||
msgstr "Адзінкавы букет"
|
||||
|
||||
msgid "Split by groups"
|
||||
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-2021 Dmitriy Yefremov
|
||||
# Copyright (C) 2018-2026 Dmitriy Yefremov
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
# Charly, 2019.
|
||||
# Dmitriy Yefremov, 2020-2023.
|
||||
# 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"
|
||||
@@ -1511,3 +1517,114 @@ msgstr "Ver."
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Installiert"
|
||||
|
||||
msgid "Use common folder for picons"
|
||||
msgstr "Gemeinsamen Ordner für Picons verwenden"
|
||||
|
||||
msgid "Activates single folder use for several profiles."
|
||||
msgstr "Aktiviert die Verwendung eines einzelnen Ordners für mehrere Profile."
|
||||
|
||||
msgid "Events"
|
||||
msgstr "Events"
|
||||
|
||||
msgid "Markers"
|
||||
msgstr "Markers"
|
||||
|
||||
msgid "IPTV only"
|
||||
msgstr "Nur IPTV"
|
||||
|
||||
msgid "No"
|
||||
msgstr "Nein"
|
||||
|
||||
msgid "Not found."
|
||||
msgstr "Nicht gefunden."
|
||||
|
||||
msgid "Current EPG cache contents."
|
||||
msgstr "Aktueller EPG-Cache-Inhalt."
|
||||
|
||||
msgid "Source error!"
|
||||
msgstr "Quellfehler!"
|
||||
|
||||
msgid "The EPG source for the favorites list is not set!"
|
||||
msgstr "Die EPG-Quelle für die Favoritenliste ist nicht eingestellt!"
|
||||
|
||||
msgid "Add to EPG sources list"
|
||||
msgstr "Zur EPG-Quellenliste hinzufügen"
|
||||
|
||||
msgid "Current bouquet"
|
||||
msgstr "Aktuelles Bouquet"
|
||||
|
||||
msgid "Single bouquet"
|
||||
msgstr "Einzel Bouquet"
|
||||
|
||||
msgid "Split by groups"
|
||||
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,12 +1,11 @@
|
||||
# Copyright (C) 2018-2022 Dmitriy Yefremov
|
||||
# Copyright (C) 2018-2026 Dmitriy Yefremov
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
# Nicola Fanghella, 2021
|
||||
# Massimo Pissarello <mapi68@gmail.com>, 2022, 2023.
|
||||
# SPDX-FileCopyrightText: 2022, 2023, 2024, 2025, 2026 Massimo Pissarello <mapi68@gmail.com>
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"PO-Revision-Date: 2023-06-02 06:05+0200\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"
|
||||
@@ -14,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.04.1\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"
|
||||
@@ -266,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"
|
||||
@@ -296,10 +299,10 @@ msgstr "Chiudi"
|
||||
|
||||
# Picons dialog
|
||||
msgid "Load providers"
|
||||
msgstr "Carica provider"
|
||||
msgstr "Carica fornitori"
|
||||
|
||||
msgid "Providers"
|
||||
msgstr "Provider"
|
||||
msgstr "Fornitori"
|
||||
|
||||
msgid "Receive picons"
|
||||
msgstr "Scarica picon"
|
||||
@@ -338,16 +341,16 @@ msgid "Path to Enigma2 picons:"
|
||||
msgstr "Persorso picon su Enigma2:"
|
||||
|
||||
msgid "Specify the correct position value for the provider!"
|
||||
msgstr "Specifica il valore di posizione corretto per il provider!"
|
||||
msgstr "Specifica il valore di posizione corretto per il fornitore!"
|
||||
|
||||
msgid "Converter between name formats"
|
||||
msgstr "Convertitore tra formati di nome"
|
||||
|
||||
msgid "Receive picons for providers"
|
||||
msgstr "Scarica picon per provider"
|
||||
msgstr "Scarica picon per fornitore"
|
||||
|
||||
msgid "Load satellite providers."
|
||||
msgstr "Carica provider satellitari"
|
||||
msgstr "Carica fornitori satellitari"
|
||||
|
||||
msgid ""
|
||||
"To automatically set the identifiers for picons,\n"
|
||||
@@ -413,7 +416,7 @@ msgid "Reference"
|
||||
msgstr "Riferimento"
|
||||
|
||||
msgid "Namespace"
|
||||
msgstr "Spazio dei nomi"
|
||||
msgstr "Namespace"
|
||||
|
||||
msgid "Flags:"
|
||||
msgstr "Flag:"
|
||||
@@ -514,8 +517,7 @@ msgstr "Non consentito in questo contesto!"
|
||||
|
||||
msgid "Please, download files from receiver or setup your path for read data!"
|
||||
msgstr ""
|
||||
"Per favore, scarica i file dal ricevitore o imposta il percorso da cui"
|
||||
" leggere i dati!"
|
||||
"Scarica i file dal ricevitore o imposta il percorso da cui leggere i dati!"
|
||||
|
||||
msgid "Reading data error!"
|
||||
msgstr "Errore lettura dati!"
|
||||
@@ -530,7 +532,7 @@ msgid "The text of marker is empty, please try again!"
|
||||
msgstr "Il testo del marcatore è vuoto, riprova!"
|
||||
|
||||
msgid "Please, select only one item!"
|
||||
msgstr "Per favore, seleziona solo un elemento!"
|
||||
msgstr "Seleziona solo un elemento!"
|
||||
|
||||
msgid "No png file is selected!"
|
||||
msgstr "Nessun file png selezionato!"
|
||||
@@ -551,7 +553,7 @@ msgid "Done!"
|
||||
msgstr "Fatto!"
|
||||
|
||||
msgid "Please, wait..."
|
||||
msgstr "Attendere prego..."
|
||||
msgstr "Attendi..."
|
||||
|
||||
msgid "Resizing..."
|
||||
msgstr "Ridimensionamento..."
|
||||
@@ -563,10 +565,10 @@ msgid "No satellite is selected!"
|
||||
msgstr "Nessun satellite selezionato!"
|
||||
|
||||
msgid "Please, select only one satellite!"
|
||||
msgstr "Per favore, seleziona solo un satellite!"
|
||||
msgstr "Seleziona solo un satellite!"
|
||||
|
||||
msgid "Please check your parameters and try again."
|
||||
msgstr "Per favore, controlla i tuoi parametri e riprova di nuovo."
|
||||
msgstr "Controlla i parametri e riprova di nuovo."
|
||||
|
||||
msgid "No satellites.xml file is selected!"
|
||||
msgstr "Nessun file satellites.xml selezionato!"
|
||||
@@ -582,7 +584,7 @@ msgstr "VLC non trovato! Controlla che sia installato!"
|
||||
|
||||
# Search unavailable streams dialog
|
||||
msgid "Please wait, streams testing in progress..."
|
||||
msgstr "Attendere prego, test degli stream in corso..."
|
||||
msgstr "Test degli stream in corso..."
|
||||
|
||||
msgid "Found"
|
||||
msgstr "Trovato"
|
||||
@@ -594,7 +596,7 @@ msgid "No changes required!"
|
||||
msgstr "Non sono richiesti cambiamenti!"
|
||||
|
||||
msgid "This list does not contains IPTV streams!"
|
||||
msgstr "L'elenco non contiene stream IPTV!"
|
||||
msgstr "Questo elenco non contiene stream IPTV!"
|
||||
|
||||
msgid "New empty configuration"
|
||||
msgstr "Nuova configurazione vuota"
|
||||
@@ -730,7 +732,7 @@ msgid "XML file"
|
||||
msgstr "File XML"
|
||||
|
||||
msgid "Use web source"
|
||||
msgstr "Utilizza fonte web"
|
||||
msgstr "Usa sorgente web"
|
||||
|
||||
msgid "Url to *.xml.gz file:"
|
||||
msgstr "Da URL a file *.xml.gz:"
|
||||
@@ -782,7 +784,7 @@ msgstr ""
|
||||
" bouquet!"
|
||||
|
||||
msgid "Use HTTP"
|
||||
msgstr "Utilizza HTTP"
|
||||
msgstr "Usa HTTP"
|
||||
|
||||
msgid "Close playback"
|
||||
msgstr "Ferma riproduzione"
|
||||
@@ -826,8 +828,8 @@ msgstr "Abilita barra di riproduzione diretta"
|
||||
|
||||
msgid "Enables direct sending and playback of media links on the receiver"
|
||||
msgstr ""
|
||||
"Consenti l'invio diretto e la riproduzione 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"
|
||||
@@ -964,7 +966,7 @@ msgstr ""
|
||||
" dell'elenco dei preferiti!"
|
||||
|
||||
msgid "Operates in standby mode or current active transponder!"
|
||||
msgstr "Funziona in modalità standby o transponder attivo corrente!"
|
||||
msgstr "Funziona in modalità standby o con il transponder attualmente attivo!"
|
||||
|
||||
msgid "No connection to the receiver!"
|
||||
msgstr "Nessuna connessione con il ricevitore!"
|
||||
@@ -1263,7 +1265,7 @@ msgstr ""
|
||||
" picon!"
|
||||
|
||||
msgid "Streams detected:"
|
||||
msgstr "Rilevati stream:"
|
||||
msgstr "Stream rilevati:"
|
||||
|
||||
msgid "Download picons"
|
||||
msgstr "Scarica picon"
|
||||
@@ -1290,7 +1292,7 @@ msgid "Mark duplicates"
|
||||
msgstr "Contrassegna duplicati"
|
||||
|
||||
msgid "Load only for selected bouquet"
|
||||
msgstr "Scarica solo per i bouquet selezionati"
|
||||
msgstr "Carica solo per i bouquet selezionati"
|
||||
|
||||
msgid "The task is canceled!"
|
||||
msgstr "L'attività è stata annullata!"
|
||||
@@ -1308,7 +1310,7 @@ msgid "Help"
|
||||
msgstr "Aiuto"
|
||||
|
||||
msgid "HTTP API is not activated. Check your settings!"
|
||||
msgstr "API HTTP non attivata. Controlla le tue impostazioni!"
|
||||
msgstr "API HTTP non attivata. Controlla le impostazioni!"
|
||||
|
||||
msgid "Add picons"
|
||||
msgstr "Aggiungi picon"
|
||||
@@ -1326,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."
|
||||
@@ -1414,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 media."
|
||||
"Abilita analisi degli URL utilizzando yt-dlp per ottenere collegamenti diretti"
|
||||
" ai contenuti multimediali."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "Permessi..."
|
||||
@@ -1427,7 +1429,7 @@ msgid "EPG *.dat file:"
|
||||
msgstr "File EPG *.dat:"
|
||||
|
||||
msgid "Use HTTP to reload data in the receiver"
|
||||
msgstr "Utilizza HTTP per ricaricare i dati nel ricevitore"
|
||||
msgstr "Usa HTTP per ricaricare i dati nel ricevitore"
|
||||
|
||||
msgid "Enable picons compression"
|
||||
msgstr "Abilita compressione picon"
|
||||
@@ -1461,15 +1463,15 @@ msgid "Region"
|
||||
msgstr "Regione"
|
||||
|
||||
msgid "Provider"
|
||||
msgstr "Provider"
|
||||
msgstr "Fornitore"
|
||||
|
||||
msgid ""
|
||||
"Enables upload as an archive if a large number of picon (> 1000) is"
|
||||
" 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\""
|
||||
@@ -1511,10 +1513,10 @@ msgid "Removed"
|
||||
msgstr "Rimosso"
|
||||
|
||||
msgid "Enables overwriting existing main list services."
|
||||
msgstr "Consente di sovrascrivere i servizi esistenti dell'elenco principale."
|
||||
msgstr "Abilita sovrascrittura dei servizi esistenti dell'elenco principale."
|
||||
|
||||
msgid "Enables skipping services import from lamedb."
|
||||
msgstr "Consente di saltare l'importazione dei servizi da lamedb."
|
||||
msgstr "Abilita salto dell'importazione dei servizi da lamedb."
|
||||
|
||||
msgid "Bouquets data only"
|
||||
msgstr "Bouquet solo dati"
|
||||
@@ -1553,10 +1555,114 @@ msgid "Selected type:"
|
||||
msgstr "Tipo selezionato:"
|
||||
|
||||
msgid "Extension Manager"
|
||||
msgstr "Manager estensioni"
|
||||
msgstr "Gestore estensioni"
|
||||
|
||||
msgid "Ver."
|
||||
msgstr "Ver."
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Installato"
|
||||
|
||||
msgid "Use common folder for picons"
|
||||
msgstr "Usa la cartella comune per i picon"
|
||||
|
||||
msgid "Activates single folder use for several profiles."
|
||||
msgstr "Attiva l'uso di una singola cartella per diversi profili."
|
||||
|
||||
msgid "Events"
|
||||
msgstr "Eventi"
|
||||
|
||||
msgid "Markers"
|
||||
msgstr "Marcatori"
|
||||
|
||||
msgid "IPTV only"
|
||||
msgstr "Solo IPTV"
|
||||
|
||||
msgid "No"
|
||||
msgstr "No"
|
||||
|
||||
msgid "Not found."
|
||||
msgstr "Non trovato."
|
||||
|
||||
msgid "Current EPG cache contents."
|
||||
msgstr "Contenuto attuale cache EPG."
|
||||
|
||||
msgid "Source error!"
|
||||
msgstr "Errore sorgente!"
|
||||
|
||||
msgid "The EPG source for the favorites list is not set!"
|
||||
msgstr "La sorgente EPG per l'elenco dei preferiti non è impostata!"
|
||||
|
||||
msgid "Add to EPG sources list"
|
||||
msgstr "Aggiungi all'elenco delle sorgenti EPG"
|
||||
|
||||
msgid "Current bouquet"
|
||||
msgstr "Bouquet attuale"
|
||||
|
||||
msgid "Single bouquet"
|
||||
msgstr "Bouquet singolo"
|
||||
|
||||
msgid "Split by groups"
|
||||
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-2023 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 "Получение пиконов для провайдеров"
|
||||
@@ -1311,7 +1317,7 @@ msgid "Sets the profile folder as default to store picons, backups, etc."
|
||||
msgstr "Устанавливает папку профиля по умолчанию для хранения пиконов, резервных копий и т. п."
|
||||
|
||||
msgid "New sub-bouquet"
|
||||
msgstr "Создать суббукет"
|
||||
msgstr "Создать подбукет"
|
||||
|
||||
msgid "Mark not presented in Bouquets"
|
||||
msgstr "Отметить отсутствующие в букетах"
|
||||
@@ -1494,3 +1500,114 @@ msgstr "Вер."
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Установлено"
|
||||
|
||||
msgid "Use common folder for picons"
|
||||
msgstr "Использовать общую папку для пиконов"
|
||||
|
||||
msgid "Activates single folder use for several profiles."
|
||||
msgstr "Активирует использование одной папки для нескольких профилей."
|
||||
|
||||
msgid "Events"
|
||||
msgstr "События"
|
||||
|
||||
msgid "Markers"
|
||||
msgstr "Маркеры"
|
||||
|
||||
msgid "IPTV only"
|
||||
msgstr "Только IPTV"
|
||||
|
||||
msgid "No"
|
||||
msgstr "Нет"
|
||||
|
||||
msgid "Not found."
|
||||
msgstr "Не найдено."
|
||||
|
||||
msgid "Current EPG cache contents."
|
||||
msgstr "Содержимое текущего EPG кэша."
|
||||
|
||||
msgid "Source error!"
|
||||
msgstr "Ошибка источника!"
|
||||
|
||||
msgid "The EPG source for the favorites list is not set!"
|
||||
msgstr "Не установлен источник EPG для списка избранного!"
|
||||
|
||||
msgid "Add to EPG sources list"
|
||||
msgstr "Добавить в список источников EPG"
|
||||
|
||||
msgid "Current bouquet"
|
||||
msgstr "Текущий букет"
|
||||
|
||||
msgid "Single bouquet"
|
||||
msgstr "Одиночный букет"
|
||||
|
||||
msgid "Split by groups"
|
||||
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-06-10 17:50+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.1\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"
|
||||
@@ -1528,3 +1534,102 @@ msgstr "Ver."
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Yüklenmiş"
|
||||
|
||||
msgid "Use common folder for picons"
|
||||
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