Compare commits

..

163 Commits

Author SHA1 Message Date
DYefremov
1d16e9e220 version update -> 3.14.1 2025-11-14 13:25:02 +03:00
DYefremov
c96b464cbc prevent double data loading
* Fixes double data loading when opening an external folder from the start page.
2025-11-14 13:22:51 +03:00
DYefremov
43821e6f50 add t2mi pid value support (#232) 2025-11-10 11:56:53 +03:00
DYefremov
e0e642db5a sk *.mo file update 2025-11-10 09:11:06 +03:00
EnoSat
2266fd4d3d Slovak translations update (#234) 2025-11-10 09:09:14 +03:00
EnoSat
6b8145c674 Add Slovak translations to desktop entry (#233) 2025-11-09 21:07:50 +03:00
DYefremov
fa89ab8608 add Slovak translation 2025-11-09 19:32:04 +03:00
EnoSat
da70b0fb18 Slovak language (#231)
Added Slovak localization.
2025-11-09 18:57:38 +03:00
DYefremov
6932465cfd version update -> 3.14.0 2025-11-02 12:08:58 +03:00
DYefremov
303fe0b1ae README update 2025-11-02 11:14:38 +03:00
DYefremov
f8dec3140c add data loading progress update 2025-11-02 11:08:59 +03:00
DYefremov
60e1e4f5e6 migration to the progress bar on data load
* It fixes long data loading on some configs.
2025-11-02 10:28:21 +03:00
DYefremov
a99d6e26db small refactoring of data update 2025-11-02 09:35:29 +03:00
DYefremov
ecf5b6399c yt-dlp init adjustment 2025-11-01 17:07:53 +03:00
DYefremov
3c04a00230 adjust getting yt icon 2025-10-08 21:35:42 +03:00
DYefremov
527a52c87b enable keep of extracted data 2025-10-01 23:22:57 +03:00
DYefremov
4bcd126947 add upload bouquets func 2025-10-01 13:56:46 +03:00
DYefremov
1a3617a6d4 improved service status bar
* Added current datta path display.
2025-09-27 15:50:50 +03:00
DYefremov
d6d7b105ec enabled data saving for ext path 2025-09-27 09:27:49 +03:00
DYefremov
eab34c5ecc small ui adjustment 2025-09-27 09:25:08 +03:00
DYefremov
3cef063aa4 upload adjustment 2025-09-26 22:09:13 +03:00
DYefremov
958320e573 separated data upload for the services tab 2025-09-23 13:13:10 +03:00
DYefremov
394b7c4c01 separated data load/upload for the satellite tab 2025-09-21 13:44:20 +03:00
DYefremov
00f492b0a2 enable multiple selection for tc views 2025-09-21 13:35:36 +03:00
DYefremov
0e0abdcf8e kos web source adjustment 2025-09-10 00:34:41 +03:00
DYefremov
037b917d3e it *.mo file update 2025-08-30 12:42:38 +03:00
mapi68
0bc85cb5fa Italian translation update (#230) 2025-08-30 12:39:56 +03:00
DYefremov
8bf6427bbd enabled data transfer from ext path 2025-08-30 00:09:31 +03:00
audi06_19
f85c1d2e0d Turkish translation update (#229) 2025-08-25 20:16:46 +03:00
DYefremov
03e2fc96ec add tooltip 2025-08-22 09:51:26 +03:00
DYefremov
f0d58c0fb4 translations update -> [be, de, ru] 2025-08-22 09:47:06 +03:00
DYefremov
1fc10f0119 add scroll to new added channel 2025-08-19 23:53:07 +03:00
DYefremov
a21f6faab2 add support for creating terrestrial and cable channels 2025-08-19 23:33:56 +03:00
DYefremov
d78cee1241 enable support for creating a new channel 2025-08-18 14:20:14 +03:00
DYefremov
145bd75776 fix internal yt client 2025-08-18 10:36:13 +03:00
DYefremov
6765fd5db7 version update -> 3.13.2 2025-08-07 22:02:38 +03:00
DYefremov
53616f95b0 сhanged output of XMLTV file loading progress
* Fixes XMLTV file loading for Windows build.
2025-08-07 21:18:59 +03:00
DYefremov
137b5acde5 BUILD_WIN.md update 2025-08-07 20:28:00 +03:00
DYefremov
17f705a4e3 it *.mo file update 2025-08-05 19:37:09 +03:00
mapi68
d68a215e2a Update Italian language (#227) 2025-08-05 19:34:03 +03:00
audi06_19
d8f67380e5 Turkish translation update (#226) 2025-08-04 07:52:53 +03:00
DYefremov
9965f3e3a5 version update -> 3.13.1 2025-08-02 10:03:15 +03:00
DYefremov
2bb0faa19e enabled delete key for alternatives 2025-08-02 09:56:53 +03:00
DYefremov
9c5cf8cebb add paste menu item for alternatives 2025-08-01 22:17:19 +03:00
DYefremov
fb20e82572 enabled paste from buffer for alternatives 2025-08-01 09:33:36 +03:00
DYefremov
ffdf5d8ce2 translations update -> [be, de, ru] 2025-07-31 00:01:57 +03:00
DYefremov
8b6f860459 add warning message
* Added warning message about errors when loading bouquets.
2025-07-30 22:26:56 +03:00
DYefremov
c747cf1275 add error counting during bouquets loading 2025-07-30 22:02:02 +03:00
DYefremov
7a71ebd188 loading bouquets refactoring
* Added data loading error skipping if bouquet file is missing.
2025-07-30 20:48:43 +03:00
DYefremov
c4847766bb enabled dnd from IPTV list to alternatives 2025-07-28 17:19:02 +03:00
DYefremov
73a611dc3c alternatives improvement
* Added display of IPTV services.
2025-07-28 00:35:06 +03:00
DYefremov
ef931bcd75 copyright update 2025-06-03 20:21:26 +03:00
DYefremov
f173587dab bootlogo manager improvement
* Added custom STB path setting.
2025-05-31 23:30:02 +03:00
DYefremov
9a0b362b91 telnetlib usage refactoring (#218) 2025-05-30 15:59:26 +03:00
DYefremov
51acb171d5 it *.mo file update 2025-05-16 12:37:44 +03:00
mapi68
b57adb43ba Italian translation update (#225)
* Add files via upload

* Add files via upload

* correct typo
2025-05-16 12:35:13 +03:00
audi06_19
bcea538c4e Turkish translation update (#224) 2025-05-11 21:34:35 +03:00
DYefremov
77281271c8 minor improvement
* Enabled name cache for multiepg mode.
2025-05-06 09:50:10 +03:00
DYefremov
5c94912f21 version update -> 3.13.0 2025-05-03 16:22:55 +03:00
DYefremov
e8f33cbee9 translations update -> [be, de, ru] 2025-05-02 18:49:01 +03:00
DYefremov
aa2b06ea27 add filtering by coding presence 2025-05-02 16:16:05 +03:00
DYefremov
5576bd8112 enabled custom ports (#221)
*  Enabled custom ports for FTP and Telnet.
2025-04-29 22:12:26 +03:00
DYefremov
551c9d5722 connection test adjustment 2025-04-29 21:45:57 +03:00
DYefremov
f6518f1ee5 enabled port values checking 2025-04-29 21:09:19 +03:00
DYefremov
20b534f723 changed type for ports 2025-04-29 21:07:10 +03:00
Thorsten Klein
82a954e1a4 Accept if lines in lamedb do not start with provider (p:) (#223)
* Accept if lines in `lamedb` do not start with provider (`p:`)

* minor adjustment
2025-04-28 22:42:21 +03:00
DYefremov
67446f0898 cas info display adjustment 2025-03-15 11:56:02 +03:00
DYefremov
39a092cb57 add cas value 2025-03-15 11:54:17 +03:00
DYefremov
4d81937779 it *.mo file update 2025-02-11 08:18:50 +03:00
audi06_19
68dc48cdbe Turkish translation update (#220) 2025-02-11 08:16:09 +03:00
mapi68
1a6be14949 translation update -> [it] (#219) 2025-02-11 08:15:47 +03:00
DYefremov
7295ec90c0 translations update -> [be, de, ru] 2025-02-08 21:25:30 +03:00
DYefremov
3a98b497c8 fix save of *.xml path changes (#217) 2025-01-21 11:43:34 +03:00
DYefremov
12bb1f0601 minor correction for EPG dialog 2025-01-19 22:25:40 +03:00
DYefremov
eebe953ac2 markers reading adjustment 2025-01-12 19:53:58 +03:00
DYefremov
741bea29e6 enabled additional name cache for EPG (#189) 2025-01-05 18:13:50 +03:00
DYefremov
97041e5799 name cache init 2025-01-04 00:58:42 +03:00
DYefremov
5ee4e18346 add EPG name cache option 2025-01-03 22:38:43 +03:00
DYefremov
de508fbfc2 add "tvg-id" parsing for *.m3u import 2025-01-02 13:06:08 +03:00
DYefremov
36aebe7f19 add deep name comparing for EPG dialog (#209) 2024-12-27 00:26:13 +03:00
DYefremov
5ac9053944 version update -> 3.12.0 2024-12-19 00:42:43 +03:00
DYefremov
ce6819d539 file naming for oscam picon converter 2024-12-19 00:38:55 +03:00
DYefremov
b13c2f321c path init for picon converter 2024-12-18 21:05:36 +03:00
DYefremov
015b6b1ccd bootlogo fmt change 2024-12-07 14:31:39 +03:00
DYefremov
911279ce09 basic support for converting to *.tpl 2024-12-05 18:06:57 +03:00
DYefremov
71ddd12541 improved picon conversion tab
* conversion for the selected bouquet
  * oscam picons ui prototype
2024-12-02 23:20:20 +03:00
DYefremov
4867b1b648 picon tab refactoring 2024-12-01 16:02:42 +03:00
DYefremov
25fba17b9c version update -> 3.11.3 2024-11-28 22:45:38 +03:00
DYefremov
f77a55eadd fix assign ref data (#209) 2024-11-28 22:37:32 +03:00
DYefremov
b6e73e5e7a pos sort for picons downloader 2024-11-26 21:08:09 +03:00
DYefremov
780bda1f12 data read adjustment 2024-11-26 20:37:20 +03:00
DYefremov
a4a44692e2 fix warn 2024-11-26 20:18:46 +03:00
DYefremov
6db03b6cac version update -> 3.11.2 2024-11-10 00:00:03 +03:00
DYefremov
a94c53a9c9 fix extra service name for bouquet 2024-11-09 23:36:06 +03:00
DYefremov
b012fccd1a minor code cleanup 2024-11-03 14:53:24 +03:00
DYefremov
4062d206b8 minor code adjustment 2024-11-03 13:50:22 +03:00
DYefremov
a1f656fbca add additional reload data cmds 2024-11-01 14:40:05 +03:00
DYefremov
84afaee1d0 add encoding blacklist for *.m3u import 2024-10-29 22:25:43 +03:00
DYefremov
08619dd182 refactoring of getting service reference
* fix wrong duplicated services filtering #208
2024-10-26 13:13:55 +03:00
DYefremov
04f27eff88 hide error status on new data open 2024-10-26 10:15:15 +03:00
DYefremov
6e706dec2d README update 2024-10-25 20:52:34 +03:00
DYefremov
3bf787b9fb version update -> 3.11.1 2024-10-03 20:49:36 +03:00
audi06_19
3b1bb80d3c Turkish translation update (#207) 2024-09-30 07:44:12 +03:00
DYefremov
05fa5eaf11 kos web source correction 2024-09-28 16:49:24 +03:00
DYefremov
b558a17d9d deb control update 2024-09-20 08:36:25 +03:00
DYefremov
0ee248a24f README update 2024-09-19 13:06:33 +03:00
DYefremov
3a368427fd web import options correction 2024-09-19 13:03:01 +03:00
DYefremov
384c30ea18 add additional events 2024-08-29 20:38:36 +03:00
DYefremov
05cf047127 export to *.m3u correction 2024-08-18 20:44:44 +03:00
DYefremov
621b090a1a it *.mo file update 2024-08-16 10:02:39 +03:00
mapi68
a8d3f39442 translation update -> [it] 2024-08-16 10:00:48 +03:00
DYefremov
02c261b4dd translations update -> [be, de, ru] 2024-08-15 12:59:54 +03:00
DYefremov
5c3532db65 add confirm dialog to bootlogo manager 2024-08-15 12:33:07 +03:00
DYefremov
fda9780de9 README update 2024-08-14 21:49:41 +03:00
DYefremov
6c5bd5d576 add support for *.mvi transfer for bootlogo manager 2024-08-14 21:03:09 +03:00
DYefremov
9c5b7a3901 add file selection for bootlogo manager 2024-08-13 16:20:51 +03:00
DYefremov
b7f312a35d add settings menu for bootlogo 2024-08-12 23:34:45 +03:00
DYefremov
9401b2a7f7 add format selection for bootlogo 2024-08-11 16:31:43 +03:00
DYefremov
682fa341d0 add load bootlogo image from local file 2024-08-10 11:32:30 +03:00
DYefremov
c9daa8a599 small refactoring 2024-08-10 10:54:31 +03:00
DYefremov
94d3d0d9ac loading logo from the box 2024-08-10 00:16:09 +03:00
DYefremov
2189997122 add boot logo manager prototype 2024-08-09 13:48:45 +03:00
DYefremov
8397efa324 streamrelay adjustment 2024-08-05 21:24:43 +03:00
DYefremov
d21f9410cd streamrelay support improvement 2024-08-04 18:33:54 +03:00
DYefremov
be9b3178e0 version update -> 3.11.0 2024-08-02 22:47:53 +03:00
DYefremov
2a8ddc093c minor fix for kos web source 2024-08-02 22:04:53 +03:00
DYefremov
fa1ec4cdcf save streamrelay change 2024-08-01 07:14:24 +03:00
DYefremov
384da95988 add streamrelay to context menu 2024-08-01 00:46:16 +03:00
DYefremov
960541b56a add basic streamrelay support (#199)
* marking services used with streamrelay in the fav list with an additional icon
2024-07-31 22:46:04 +03:00
DYefremov
396d10a805 extension manager ui adjustment 2024-07-27 23:41:07 +03:00
DYefremov
30e1c63a47 extension manager ui adjustment 2024-07-26 14:23:25 +03:00
DYefremov
ef7e35378d backup dialog ui adjustment 2024-07-26 14:18:54 +03:00
DYefremov
0a1bbab7d0 web import dialog redesign 2024-07-12 00:10:58 +03:00
DYefremov
65502018a0 add split satellites by band for web import (#204) 2024-07-02 19:28:03 +03:00
DYefremov
cc20042001 web import adjustments 2024-07-02 15:51:12 +03:00
DYefremov
50c2e831ce extension manager adjustment 2024-07-02 14:38:19 +03:00
DYefremov
ea91c39769 fix bouquet hiding status 2024-06-20 15:32:47 +03:00
DYefremov
3dab8ef7b7 version update -> 3.10.2 2024-06-17 16:16:41 +03:00
DYefremov
dd1a543e5c allowed data extraction from startup page 2024-06-16 18:12:32 +03:00
DYefremov
0966489024 fix reading some bouquets (#202)
* additional format for reading bouquet file name
2024-06-16 18:06:22 +03:00
DYefremov
052187359d version update -> 3.10.1 2024-06-08 17:44:11 +03:00
DYefremov
6ca6867ea9 small adjustment (#201) 2024-06-08 17:38:06 +03:00
DYefremov
d9cdc6458c fix services duplication (#201)
* Fixes service double loading when changing profile on start page.
2024-06-08 15:48:55 +03:00
DYefremov
70b9851324 minor ui correction
* IPTV list config dialog adjustment for macOS
2024-05-17 22:52:46 +03:00
DYefremov
2a3b558d83 adjustment for LyngSat web source
* default values for ONID-TID if not present (#200)
2024-05-09 19:13:52 +03:00
DYefremov
21ea841f34 version update -> 3.10.0 2024-05-07 18:53:52 +03:00
DYefremov
1db0ce3fc5 README update 2024-05-07 18:42:01 +03:00
DYefremov
2804a9bc54 change playback keyboard shortcuts 2024-05-07 18:41:36 +03:00
DYefremov
8976f42974 enabled playback mode for the main services list (#198) 2024-05-07 17:56:00 +03:00
DYefremov
8330104f3c add last config load flag 2024-03-29 22:22:05 +03:00
DYefremov
3ede2e2b07 fix -> mark not present in bouquets (#197) 2024-03-29 20:35:48 +03:00
DYefremov
dd796c0f88 version update -> 3.9.2-b 2024-03-16 17:58:52 +03:00
DYefremov
f3a432c002 add optional keep for backup
* prevent *.xml deletion (#196)
2024-03-11 00:38:25 +03:00
DYefremov
4c1cdc4850 increase pos width for sat dialog 2024-03-06 20:53:14 +03:00
DYefremov
a062d74e0e BUILD_WIN.md update 2024-03-05 22:53:41 +03:00
DYefremov
7bf36c8d6d minor fix to add a timer 2024-03-04 13:38:36 +03:00
DYefremov
6aad0344c8 timer dialog adjustment 2024-03-03 19:47:54 +03:00
DYefremov
bb4665d180 default emblem adjustment 2024-03-03 11:53:57 +03:00
DYefremov
a455c4569d version update -> 3.9.1-b 2024-03-03 10:05:23 +03:00
DYefremov
8d5af301fb dialogs ui adjustments 2024-03-02 13:08:54 +03:00
DYefremov
f342b99769 update *.desktop file for deb package 2024-03-02 12:29:42 +03:00
DYefremov
a0612e6a98 separate data loading (#196) 2024-03-02 11:57:20 +03:00
DYefremov
60b8f7642d lamedb5 reading correction (#194) 2024-03-01 22:19:34 +03:00
68 changed files with 4804 additions and 1293 deletions

View File

@@ -1,6 +1,6 @@
MIT License
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

View File

@@ -41,10 +41,7 @@ Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
* **Ctrl + Alt + R** - rename for bouquet.
* **Ctrl + S, T** in Satellites edit tool for create satellite or transponder.
* **Ctrl + L** - parental lock.
* **Ctrl + H** - hide/skip.
* **Ctrl + P** - start play IPTV or other stream in the bouquet list.
* **Ctrl + Z** - switch(**zap**) the channel(works when the HTTP API is enabled, Enigma2 only).
* **Ctrl + W** - switch to the channel and watch in the program.
* **Ctrl + H** - hide/skip.
* **Space** - select/deselect.
* **Left/Right** - remove selection.
* **Ctrl + Up, Down, PageUp, PageDown, Home, End**- move selected items in the list.
@@ -56,13 +53,16 @@ Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
* **Ctrl + Shift + F** - show/hide filter bar.
* **Ctrl + T** - show/hide built-in Telnet client.
* **Ctrl + Shift + L** - show/hide logging panel.
* **Shift + P** - start play IPTV or other stream in the bouquet list.
* **Shift + Z** - switch(**zap**) the channel(works when the HTTP API is enabled, Enigma2 only).
* **Shift + W** - switch to the channel and watch in the program.
For **multiple** selection with the mouse, press and hold the **Ctrl** key!
## Minimum requirements
*Python >= 3.6, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.*
***Optional:** python3-pil, python3-chardet.*
***Optional:** python3-pil, python3-chardet, ffmpeg.*
## Installation and Launch
* ### Linux
To start the program, in most cases it is enough to download the [archive](https://github.com/DYefremov/DemonEditor/archive/master.zip), unpack
@@ -77,9 +77,11 @@ A ready-made [package](https://aur.archlinux.org/packages/demoneditor-bin) is al
**This program can be run on macOS.**
To run the program on macOS, you need to install [Homebrew](https://brew.sh/).
Then install the required components via terminal:
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme python-requests gtksourceview3```
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme gtksourceview3```
*Optional:* ```brew install pillow python-chardet```
```pip3 install requests telnetlib-313-and-up --break-system-packages```
*Optional:* ```brew install pillow python-chardet ffmpeg```
Launch is similar to Linux.
@@ -99,15 +101,12 @@ THIS SOFTWARE COMES WITH ABSOLUTELY NO WARRANTY.
AUTHOR IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY CONNECTION WITH THIS SOFTWARE.
## Important
The program is tested only with [openATV](https://www.opena.tv/) image and **Formuler F1** receiver in [Linux Mint](https://linuxmint.com/) (MATE 64-bit) distribution!
Support for DVB-T/T2 and DVB-C channels for Neutrino is not fully implemented and has an experimental status.
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support! For version **3** is only read mode available. When saving, version **4** format is used instead.
When using the multiple import feature, from *lamedb* will be taken data **only for channels that are in the selected bouquets!**
If you need full set of the data, including *[satellites, terrestrial, cables].xml* (current files will be overwritten),
just load your data via *"File/Open"* and press *"Save"*. When importing separate bouquet files, only those services
(excluding IPTV) that are in the **current open lamedb** (main list of services) will be imported.
When importing separate bouquet files, only those services (excluding IPTV) that are in the **current open lamedb** (main list of services) will be imported.
**The built-in Telnet client does not support ANSI escape sequences!**

View File

@@ -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")

View File

@@ -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):

View File

@@ -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"}

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2024 Dmitriy Yefremov
# Copyright (c) 2018-2025 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -27,6 +27,7 @@
""" Module for working with Enigma2 bouquets. """
import os.path
import re
from collections import Counter
from enum import Enum
@@ -41,6 +42,24 @@ _DEFAULT_BOUQUET_NAME = "favourites"
_MARKER_PREFIX = "[MARKER!] "
class ServiceType(Enum):
SERVICE = "0"
BOUQUET = "7" # Sub bouquet.
MARKER = "64"
SPACE = "832"
ALT = "134" # Alternatives.
UDP = "256"
HIDDEN = "519" # Skip, hide.
@classmethod
def _missing_(cls, value):
log("Error. No matching service type [{} {}] was found.".format(cls.__name__, value))
return cls.SERVICE
def __str__(self):
return self.value
class BouquetsWriter:
""" Class for creating and writing bouquet files.
@@ -138,11 +157,10 @@ class BouquetsWriter:
bouquet.append(self._ALT.format(f_name))
self.write_bouquet(f"{p.parent}/{f_name}", srv.service, services)
else:
data = to_bouquet_id(srv)
if srv.service:
bouquet.append(f"#SERVICE {data}:{srv.service}\n#DESCRIPTION {srv.service}\n")
bouquet.append(f"#SERVICE {srv.fav_id}:{srv.service}\n#DESCRIPTION {srv.service}\n")
else:
bouquet.append(f"#SERVICE {data}\n")
bouquet.append(f"#SERVICE {srv.fav_id}\n")
with open(path, "w", encoding="utf-8", newline="\n") as file:
file.writelines(bouquet)
@@ -160,34 +178,22 @@ class BouquetsWriter:
file.writelines(bouquet)
class ServiceType(Enum):
SERVICE = "0"
BOUQUET = "7" # Sub bouquet.
MARKER = "64"
SPACE = "832"
ALT = "134" # Alternatives.
UDP = "256"
HIDDEN = "519" # Skip, hide.
@classmethod
def _missing_(cls, value):
log("Error. No matching service type [{} {}] was found.".format(cls.__name__, value))
return cls.SERVICE
def __str__(self):
return self.value
class BouquetsReader:
""" Class for reading and parsing bouquets. """
_BQ_PAT = re.compile(r".*FROM BOUQUET\s+\"((.*bouquet|alternatives)?\.?([\w-]+)\.?(\w+)?)\"\s+.*$", re.IGNORECASE)
_BQ_PAT2 = re.compile(r"#SERVICE:+\s+(?:[0-9a-f]+:+)+([^:]+[.](?:tv|radio))$", re.IGNORECASE)
_BQ_POST_PAT = re.compile(r".*FROM BOUQUET\s+\"((.*bouquet|alternatives)?\.?(.*)\.?(\w+)?)\"\s+.*$", re.IGNORECASE)
_STREAM_TYPES = {"4097", "5001", "5002", "8193", "8739"}
__slots__ = ["_path"]
__slots__ = ["_path", "_errors"]
def __init__(self, path):
def __init__(self, path=""):
self._path = path
self._errors = 0
@property
def errors(self):
return self._errors
def get(self):
""" Returns a tuple of TV and Radio bouquets. """
@@ -199,6 +205,7 @@ class BouquetsReader:
_, _, bqs_name = line.partition("#NAME")
if not bqs_name:
log(f"No bouquets name found in '{bq_name}'")
self._errors += 1
bqs_name = "Bouquets (TV)" if bq_type == BqType.TV.value else "Bouquets (Radio)"
bouquets = Bouquets(bqs_name.strip(), bq_type, [])
@@ -207,9 +214,10 @@ class BouquetsReader:
for line in file.readlines():
if "#SERVICE" in line:
mt = re.match(self._BQ_PAT, line)
s_data = line.split(":")
s_type = ServiceType(s_data[1])
s_type = ServiceType.BOUQUET
mt = re.match(self._BQ_PAT, line) or re.match(self._BQ_PAT2, line)
if not mt:
# Additional file name checking.
mt = re.match(self._BQ_POST_PAT, line)
@@ -217,7 +225,14 @@ class BouquetsReader:
log(f"Warning: The bouquet file name may be formed incorrectly. -> {mt.group(1)}")
if mt:
file_name, prefix, b_name = mt.group(1), mt.group(2), mt.group(3)
if len(mt.groups()) > 1:
file_name, prefix, b_name = mt.group(1), mt.group(2), mt.group(3)
s_type = ServiceType(s_data[1])
s_data[:2] = "10"
else:
file_name, prefix, b_name = mt.group(1), "", ""
s_type = ServiceType(s_data[2])
if b_name in b_names:
log(f"The list of bouquets contains duplicate [{b_name}] names!")
else:
@@ -231,7 +246,6 @@ class BouquetsReader:
else:
real_b_names[rb_name] = 0
# Locked, hidden.
s_data[:2] = "10"
locked = ":".join(s_data).rstrip()
hidden = s_type is ServiceType.HIDDEN
bouquets[2].append(Bouquet(rb_name, bq_type, services, locked, hidden, file_name))
@@ -241,21 +255,30 @@ class BouquetsReader:
bouquets[2].append(Bouquet(b_name, BqType.MARKER.value, [], None, None, line.strip()))
else:
log(f"Unsupported or invalid data format: [{line}].")
self._errors += 1
else:
log(f"Unsupported or invalid line format: [{line}].")
self._errors += 1
return bouquets
@staticmethod
def get_bouquet(path, f_name, bq_name):
def get_bouquet(self, path, f_name, bq_name):
""" Parsing services ids from bouquet file. """
with open(f"{path}{f_name}", encoding="utf-8", errors="replace") as file:
bq_file = f"{path}{f_name}"
services = []
if not os.path.isfile(bq_file):
log(f"Bouquet reading error: No such bouquet [{bq_name}] file -> '{f_name}'.")
self._errors += 1
return f"! -> {bq_name}", services
with open(bq_file, encoding="utf-8", errors="replace") as file:
chs_list = file.read()
services = []
srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering ['']
# May come across empty[wrong] files!
if not srvs:
log(f"Bouquet file '{f_name}' is empty or wrong!")
self._errors += 1
return f"{bq_name} [empty]", services
bq_name = srvs.pop(0)
@@ -265,50 +288,43 @@ class BouquetsReader:
data_len = len(srv_data)
if data_len < 10:
log(f"The bouquet [{bq_name}] service [{num}] has the wrong data format: [{srv}]")
self._errors += 1
continue
s_type = ServiceType(srv_data[1])
if s_type is ServiceType.MARKER:
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num))
m_data, sep, desc = srv_data[-1].partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else m_data, BqServiceType.MARKER, srv, num))
elif s_type is ServiceType.SPACE:
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
elif s_type is ServiceType.ALT:
alt = re.match(BouquetsReader._BQ_PAT, srv)
alt = re.match(self._BQ_PAT, srv)
if alt:
af_name, alt_name = alt.group(1), alt.group(3)
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, af_name, alt_name)
alt_bq_name, alt_srvs = self.get_bouquet(path, af_name, alt_name)
services.append(BouquetService(alt_bq_name, BqServiceType.ALT, alt_name, tuple(alt_srvs)))
elif s_type is ServiceType.BOUQUET:
sub = re.match(BouquetsReader._BQ_PAT, srv)
sub = re.match(self._BQ_PAT, srv)
if sub:
sf_name, sub_name, sub_type = sub.group(1), sub.group(3), sub.group(4)
sub_bq_name, sub_srvs = BouquetsReader.get_bouquet(path, sf_name, sub_name)
sub_bq_name, sub_srvs = self.get_bouquet(path, sf_name, sub_name)
bq = Bouquet(sub_bq_name, sub_type, tuple(sub_srvs), None, None, sf_name)
services.append(BouquetService(sub_bq_name, BqServiceType.BOUQUET, bq, num))
elif srv_data[0].strip() in BouquetsReader._STREAM_TYPES or srv_data[10].startswith(("http", "rtsp")):
elif srv_data[0].strip() in self._STREAM_TYPES or srv_data[10].startswith(("http", "rtsp")):
stream_data, sep, desc = srv.partition("#DESCRIPTION")
desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip()
services.append(BouquetService(desc, BqServiceType.IPTV, srv, num))
else:
fav_id = f"{srv_data[3]}:{srv_data[4]}:{srv_data[5]}:{srv_data[6]}"
fav_id = srv.strip().upper()
name = None
if data_len == 12:
fav_id = f":".join(srv_data[:11])
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num))
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id, num))
return bq_name.lstrip("#NAME").strip(), services
def to_bouquet_id(srv):
""" Creates bouquet channel id. """
data_type = srv.data_id
if data_type and len(data_type) > 4:
data_type = int(srv.data_id.split(":")[4])
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, srv.fav_id)
if __name__ == "__main__":
pass

View File

@@ -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()

View 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

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2024 Dmitriy Yefremov
# Copyright (c) 2018-2025 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -42,6 +42,8 @@ ENIGMA2_FAV_ID_FORMAT = " {}:{}:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTI
MARKER_FORMAT = " 1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n"
PICON_FORMAT = "{}_{}_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png"
ENCODING_BLACKLIST = {"MacRoman"}
class StreamType(Enum):
DVB_TS = "1"
@@ -73,6 +75,7 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
else:
enc = chardet.detect(data)
encoding = enc.get("encoding", "utf-8")
encoding = "utf-8" if encoding in ENCODING_BLACKLIST else encoding
aggr = [None] * 10
s_aggr = aggr[: -3]
@@ -99,6 +102,7 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
data = dict(pattern.findall(line))
name = data.get("tvg-name", name)
picon = data.get("tvg-logo", None)
epg_id = data.get("tvg-id", None)
if s_type is SettingsType.ENIGMA_2:
group = data.get("group-title", None)
@@ -109,6 +113,7 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
params[0] = sid_counter
sid_counter += 1
fav_id = get_fav_id(url, name, s_type, params)
if s_type is SettingsType.ENIGMA_2:
p_id = get_picon_id(params)
if group not in groups:
@@ -120,7 +125,7 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
services.append(Service(None, None, None, group, *aggr[0:3], m_name, *aggr, m_id, None))
if all((name, url, fav_id)):
services.append(Service(None, None, IPTV_ICON, name, *aggr[0:2], group,
services.append(Service(epg_id, None, IPTV_ICON, name, *aggr[0:2], group,
st, picon, p_id, *s_aggr, url, fav_id, None))
else:
log(f"*.m3u* parse error ['{path}']: name[{name}], url[{url}], fav id[{fav_id}]")
@@ -142,7 +147,11 @@ def export_to_m3u(path, bouquet, s_type, url=None):
lines.append(f"#EXTINF:-1,{s.name}\n")
lines.append(current_grp) if current_grp else None
u = res.group(1)
lines.append(f"{unquote(u[:u.rfind(':')]) if s_type is SettingsType.ENIGMA_2 else u}\n")
if s_type is SettingsType.ENIGMA_2:
index = u.rfind(":")
lines.append(f"{unquote(u[:index] if index > 0 else u)}\n")
else:
lines.append(f"{u}\n")
elif srv_type is BqServiceType.MARKER:
current_grp = f"#EXTGRP:{s.name}\n"
elif srv_type is BqServiceType.DEFAULT and url:

View File

@@ -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):

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
# Copyright (c) 2018-2025 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -56,9 +56,9 @@ class Defaults:
USER = "root"
PASSWORD = ""
HOST = "127.0.0.1"
FTP_PORT = "21"
HTTP_PORT = "80"
TELNET_PORT = "23"
FTP_PORT = 21
HTTP_PORT = 80
TELNET_PORT = 23
HTTP_USE_SSL = False
# Enigma2.
BOX_SERVICES_PATH = "/etc/enigma2/"
@@ -305,11 +305,11 @@ class Settings:
self._cp_settings["hosts"] = value
@property
def port(self):
return self._cp_settings.get("port", self.get_default("port"))
def port(self) -> int:
return int(self._cp_settings.get("port", self.get_default("port")))
@port.setter
def port(self, value):
def port(self, value: int):
self._cp_settings["port"] = value
@property
@@ -329,19 +329,19 @@ class Settings:
self._cp_settings["password"] = value
@property
def http_port(self):
return self._cp_settings.get("http_port", self.get_default("http_port"))
def http_port(self) -> int:
return int(self._cp_settings.get("http_port", self.get_default("http_port")))
@http_port.setter
def http_port(self, value):
def http_port(self, value: int):
self._cp_settings["http_port"] = value
@property
def http_timeout(self):
def http_timeout(self) -> int:
return self._cp_settings.get("http_timeout", self.get_default("http_timeout"))
@http_timeout.setter
def http_timeout(self, value):
def http_timeout(self, value: int):
self._cp_settings["http_timeout"] = value
@property
@@ -353,11 +353,11 @@ class Settings:
self._cp_settings["http_use_ssl"] = value
@property
def telnet_port(self):
return self._cp_settings.get("telnet_port", self.get_default("telnet_port"))
def telnet_port(self) -> int:
return int(self._cp_settings.get("telnet_port", self.get_default("telnet_port")))
@telnet_port.setter
def telnet_port(self, value):
def telnet_port(self, value: int):
self._cp_settings["telnet_port"] = value
@property
@@ -604,6 +604,15 @@ class Settings:
def epg_xml_sources(self, value):
self._cp_settings["epg_xml_sources"] = value
@property
def enable_epg_name_cache(self):
""" Enables additional name cache for EPG. """
return self._settings.get("enable_epg_name_cache", False)
@enable_epg_name_cache.setter
def enable_epg_name_cache(self, value):
self._settings["enable_epg_name_cache"] = value
# *********** FTP ************ #
@property

View File

@@ -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
@@ -32,7 +32,6 @@ import os
import re
import shutil
import struct
import sys
import xml.etree.ElementTree as ET
from collections import namedtuple, defaultdict
from datetime import datetime, timezone
@@ -275,15 +274,16 @@ class XmlTvReader(Reader):
with NamedTemporaryFile(suffix=suf, delete=not IS_WIN) as tf:
downloaded = 0
data_size = int(data_size)
log("Downloading XMLTV file...")
completed = set()
for data in resp.iter_content(chunk_size=1024):
downloaded += len(data)
tf.write(data)
done = int(50 * downloaded / data_size)
sys.stdout.write(f"\rDownloading XMLTV file [{'=' * done}{' ' * (50 - done)}]")
sys.stdout.flush()
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)
sys.stdout.write("\n")
os.makedirs(os.path.dirname(self._path), exist_ok=True)
@@ -324,7 +324,7 @@ class XmlTvReader(Reader):
utc = dt.timestamp()
offset = datetime.now() - dt
for srv in filter(lambda s: any(name in names for name in s.names), self._cache.values()):
for srv in filter(lambda s: s.id in names or any(name in names for name in s.names), self._cache.values()):
[self.process_event(ev, events, offset, srv) for ev in filter(lambda s: s.duration > utc, srv.events)]
return events

View File

@@ -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,13 +32,14 @@ import re
import shutil
import subprocess
from collections import namedtuple
from enum import IntEnum
from html.parser import HTMLParser
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 +52,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). """
@@ -304,7 +311,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 +342,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 +355,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 +449,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 +482,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 +511,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__":

View File

@@ -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

View File

@@ -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. """

View File

@@ -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>

View File

@@ -41,6 +41,12 @@ from app.ui.dialogs import show_dialog, DialogType, get_builder
from app.ui.main_helper import append_text_to_tview, show_info_bar_message
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, HeaderBar
KEEP_DATA = {"satellites.xml",
"terrestrial.xml",
"cables.xml",
"whitelist",
"whitelist_streamrelay"}
class RestoreType(Enum):
BOUQUETS = 0
@@ -232,18 +238,19 @@ class BackupDialog:
self.restore(RestoreType.BOUQUETS)
def backup_data(path, backup_path, move=True):
def backup_data(path, backup_path, move=True, keep=None):
""" Creating data backup from a folder at the specified path
Returns full path to the compressed file.
"""
keep = keep or KEEP_DATA
backup_path = f"{backup_path}{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}{SEP}"
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
os.makedirs(os.path.dirname(path), exist_ok=True)
# Backup files in data dir.
for file in filter(lambda f: os.path.isfile(os.path.join(path, f)), os.listdir(path)):
src, dst = os.path.join(path, file), backup_path + file
shutil.move(src, dst) if move else shutil.copy(src, dst)
shutil.move(src, dst) if move and file not in keep else shutil.copy(src, dst)
# Compressing to zip and delete remaining files.
zip_file = shutil.make_archive(backup_path.rstrip(SEP), "zip", backup_path)
shutil.rmtree(backup_path)
@@ -259,7 +266,7 @@ def restore_data(src, dst):
def clear_data_path(path):
""" Clearing data at the specified path excluding *.xml file. """
for file in filter(lambda f: not f.endswith(".xml") and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
for file in filter(lambda f: f not in KEEP_DATA and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
os.remove(os.path.join(path, file))

View 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>

398
app/ui/bootlogo.py Normal file
View File

@@ -0,0 +1,398 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# 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
# 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
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
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)
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())
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)
if Path(ffmpeg_output).exists():
os.rename(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)
@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

View File

@@ -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:

View File

@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor. -->
<!-- interface-copyright 2018-2024 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2025 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkAboutDialog" id="about_dialog">
<property name="can_focus">False</property>
@@ -40,8 +40,8 @@ Author: Dmitriy Yefremov
<property name="icon_name">system-help</property>
<property name="type_hint">normal</property>
<property name="program_name">DemonEditor</property>
<property name="version">3.9.0 Beta</property>
<property name="copyright">2018-2024 Dmitriy Yefremov
<property name="version">3.14.1 Beta</property>
<property name="copyright">2018-2025 Dmitriy Yefremov
</property>
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor.</property>
<property name="website">https://dyefremov.github.io/DemonEditor/</property>

View File

@@ -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
@@ -47,7 +47,7 @@ class BaseDialog(Gtk.Dialog):
title=translate(title),
modal=True,
resizable=False,
default_width=240,
default_width=255,
skip_taskbar_hint=True,
skip_pager_hint=True,
destroy_with_parent=True,
@@ -61,12 +61,12 @@ class Dialog(Enum):
MESSAGE = """
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.16"/>
<requires lib="gtk+" version="3.22"/>
<object class="GtkMessageDialog" id="message_dialog">
<property name="use-header-bar">{use_header}</property>
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="width_request">250</property>
<property name="width_request">255</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="skip_taskbar_hint">True</property>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2
<!-- Generated with glade 3.40.0
The MIT License (MIT)
@@ -87,9 +87,9 @@ Author: Dmitriy Yefremov
<property name="can-focus">False</property>
<child>
<object class="GtkMenuItem" id="bouquet_assign_ref_popup_item">
<property name="label" translatable="yes">Assign</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Assign</property>
<signal name="activate" handler="on_assign_ref" swapped="no"/>
<accelerator key="v" signal="activate" modifiers="Primary"/>
</object>
@@ -217,12 +217,11 @@ Author: Dmitriy Yefremov
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-left">10</property>
<property name="margin-right">10</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
@@ -241,7 +240,7 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="source_selection_box">
<property name="visible">True</property>
@@ -293,7 +292,7 @@ Author: Dmitriy Yefremov
<property name="sensitive">False</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkFileChooserButton" id="xml_chooser_button">
<property name="visible">True</property>
@@ -399,6 +398,7 @@ Author: Dmitriy Yefremov
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Filter by presence in the epg.dat file.</property>
<property name="margin-top">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
@@ -432,13 +432,52 @@ Author: Dmitriy Yefremov
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Enables deeper name matching. Possible inaccuracies!</property>
<property name="margin-top">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Enable deep name comparison</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="enable_deep_comparing_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="halign">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkBox" id="epg_dat_source_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
@@ -551,7 +590,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
<property name="position">4</property>
</packing>
</child>
</object>
@@ -565,6 +604,7 @@ Author: Dmitriy Yefremov
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
</object>
<packing>
<property name="expand">False</property>
@@ -576,6 +616,8 @@ Author: Dmitriy Yefremov
<object class="GtkBox" id="actions_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="spacing">5</property>
<property name="homogeneous">True</property>
<child>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2024 Dmitriy Yefremov
# Copyright (c) 2018-2025 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,6 +29,7 @@
""" Module for working with EPG. """
import abc
import gzip
import json
import locale
import os
import re
@@ -43,7 +44,7 @@ from urllib.parse import quote
from gi.repository import GLib
from app.commons import run_idle, run_task, run_with_delay
from app.commons import run_idle, run_task, run_with_delay, log
from app.connections import download_data, DownloadType, HttpAPI
from app.eparser.ecommons import BouquetService, BqServiceType
from app.settings import SEP, EpgSource, IS_WIN
@@ -61,7 +62,80 @@ class RefsSource(Enum):
XML = 1
class StringComparer:
""" Additional string similarity comparer. """
class ALG(Enum):
JARO = "Jaro-Winkler"
@staticmethod
def jaro_distance(s1, s2):
""" Returns [Jaro-Winkler] distance of two strings."""
if s1 == s2:
return 1.0
len1, len2 = len(s1), len(s2)
if len1 == 0 or len2 == 0:
return 0.0
match = 0
max_dist = (max(len(s1), len(s2)) // 2) - 1
s1_hash = [0] * len(s1)
s2_hash = [0] * len(s2)
for i in range(len1):
for j in range(max(0, i - max_dist), min(len2, i + max_dist + 1)):
if s1[i] == s2[j] and s2_hash[j] == 0:
s1_hash[i] = 1
s2_hash[j] = 1
match += 1
break
if match == 0:
return 0.0
t = 0
point = 0
for i in range(len1):
if s1_hash[i]:
while s2_hash[point] == 0:
point += 1
if s1[i] != s2[point]:
point += 1
t += 1
else:
point += 1
t /= 2
return (match / len1 + match / len2 + (match - t) / match) / 3.0
@staticmethod
def is_similar(s1, s2, alg, max_ch=4, ratio=0.92):
""" Returns similarity of two strings. """
if alg is StringComparer.ALG.JARO:
dist = StringComparer.jaro_distance(s1, s2)
if dist > 0.7:
prefix = 0
for i in range(min(len(s1), len(s2))):
if s1[i] == s2[i]:
prefix += 1
else:
break
prefix = min(max_ch, prefix) # Maximum of [max_ch] characters are allowed in prefix!
dist += 0.1 * prefix * (1 - dist)
return dist > ratio
else:
raise ValueError(f"This algorithm [{alg}] is not supported!")
class EpgCache(abc.ABC):
_CACHE_FILE = "epg-name-cache"
NAME_CACHE = {} # service name -> id (tvg-id for *.m3u)
def __init__(self, app):
super().__init__()
self.events = {}
@@ -83,6 +157,9 @@ class EpgCache(abc.ABC):
self._app.connect("epg-settings-changed", self.on_settings_changed)
self._app.connect("task-canceled", self.on_xml_load_cancel)
if self._app.app_settings.enable_epg_name_cache:
self.init_name_cache(self._app.app_settings.default_data_path)
@property
def current_reader(self):
return self._reader
@@ -110,16 +187,20 @@ class EpgCache(abc.ABC):
self._canceled = True
@abc.abstractmethod
def reset(self) -> None: pass
def reset(self) -> None:
pass
@abc.abstractmethod
def update_epg_data(self) -> bool: pass
def update_epg_data(self) -> bool:
pass
@abc.abstractmethod
def get_current_event(self, service_name) -> EpgEvent: pass
def get_current_event(self, service_name) -> EpgEvent:
pass
@abc.abstractmethod
def get_current_events(self, service_name) -> list: pass
def get_current_events(self, service_name) -> list:
pass
@staticmethod
def get_gz_file_name(url, path):
@@ -129,6 +210,30 @@ class EpgCache(abc.ABC):
f_sha1 = sha1(url.encode("utf-8", errors="ignore")).hexdigest()
return f"{path}epg{os.sep}{f_sha1}_epg.gz"
@staticmethod
@run_task
def update_name_cache(path, values):
EpgCache.NAME_CACHE.update(values)
log(f"[{EpgCache.__name__}] Updating name cache...")
f_name = f"{path}{EpgCache._CACHE_FILE}"
with open(f_name, "w", encoding="utf-8") as cf:
log(f"[{EpgCache.__name__}] Dumping name cache... -> [{f_name}]")
json.dump(EpgCache.NAME_CACHE, cf)
@staticmethod
@run_task
def init_name_cache(path):
f_name = f"{path}{EpgCache._CACHE_FILE}"
if not os.path.isfile(f_name):
return
log(f"[{EpgCache.__name__}] Name cache init...")
try:
with open(f_name, "r", encoding="utf-8") as cf:
EpgCache.NAME_CACHE.update(json.load(cf))
except Exception as e:
log(f"[{EpgCache.__name__}] Name cache init error: {e}")
class FavEpgCache(EpgCache):
@@ -212,13 +317,18 @@ class FavEpgCache(EpgCache):
def update_xml_data(self):
services = self._app.current_services
names = {services[s].service for s in self._app.current_bouquets.get(self._current_bq, []) if s in services}
if self._app.app_settings.enable_epg_name_cache:
id_names = set(filter(lambda n: n in EpgCache.NAME_CACHE, names))
names -= id_names
names.update({EpgCache.NAME_CACHE.get(n) for n in id_names})
for name, events in self._reader.get_current_events(names).items():
ev = min(events, key=lambda x: x.start, default=None)
if ev:
self.events[name] = ev
def get_current_event(self, service_name):
return self.events.get(service_name, EpgEvent())
return self.events.get(EpgCache.NAME_CACHE.get(service_name, service_name), EpgEvent())
def get_current_events(self, service_name):
return [EpgEvent()]
@@ -312,6 +422,12 @@ class TabEpgCache(EpgCache):
def update_epg_data(self) -> bool:
services = self._app.current_services
names = {services[s].service for s in chain.from_iterable(self._app.current_bouquets.values()) if s in services}
if self._app.app_settings.enable_epg_name_cache:
id_names = set(filter(lambda n: n in EpgCache.NAME_CACHE, names))
names -= id_names
names.update({EpgCache.NAME_CACHE.get(n) for n in id_names})
for name, events in self._reader.get_current_events(names).items():
self.events[name] = events
@@ -323,7 +439,7 @@ class TabEpgCache(EpgCache):
pass
def get_current_events(self, service_name) -> list:
return self.events.get(service_name, [])
return self.events.get(EpgCache.NAME_CACHE.get(service_name, service_name), [])
class EpgSettingsPopover(Gtk.Popover):
@@ -588,7 +704,11 @@ class EpgTool(Gtk.Box):
return
if self._multi_epg_button.get_active():
path = next((r.path for r in self._model if r[-1].get("e2eventservicename", None) == srv.service), None)
name = srv.service
if self._app.app_settings.enable_epg_name_cache:
name = EpgCache.NAME_CACHE.get(name, name)
path = next((r.path for r in self._model if r[-1].get("e2eventservicename", None) == name), None)
scroll_to(path, self._view) if path else None
else:
self._app.wait_dialog.show()
@@ -860,6 +980,8 @@ class EpgDialog:
self._left_action_box = builder.get_object("left_action_box")
self._xml_download_progress_bar = builder.get_object("xml_download_progress_bar")
self._src_load_spinner = builder.get_object("src_load_spinner")
self._auto_config_button = builder.get_object("auto_config_button")
self._enable_deep_comparing_switch = builder.get_object("enable_deep_comparing_switch")
# Filter
self._filter_bar = builder.get_object("filter_bar")
self._filter_entry = builder.get_object("filter_entry")
@@ -1149,12 +1271,17 @@ class EpgDialog:
num=r[Column.FAV_NUM])
services.append(srv)
ChannelsParser.write_refs_to_xml("{}{}.xml".format(response, self._bouquet_name), services)
ChannelsParser.write_refs_to_xml(f"{response}{self._bouquet_name}.xml", services)
self.show_info_message(translate("Done!"), Gtk.MessageType.INFO)
@run_idle
def on_auto_configuration(self, item):
gen = self.auto_configuration()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def auto_configuration(self):
""" Simple mapping of services by name. """
self._auto_config_button.set_sensitive(False)
use_cyrillic = locale.getdefaultlocale()[0] in ("ru_RU", "be_BY", "uk_UA", "sr_RS")
tr = None
if use_cyrillic:
@@ -1182,20 +1309,28 @@ class EpgDialog:
if ref:
self.assign_data(r, ref, True)
success_count += 1
self._bouquet_epg_count_label.set_text(str(success_count))
yield True
else:
not_founded[name] = r
# Additional attempt to search in the remaining elements
use_deep = self._enable_deep_comparing_switch.get_active()
for n in not_founded:
for k in source:
if k.startswith(n):
if StringComparer.is_similar(k, n, StringComparer.ALG.JARO) if use_deep else k.startswith(n):
self.assign_data(not_founded[n], source[k], True)
success_count += 1
self._bouquet_epg_count_label.set_text(str(success_count))
break
yield True
self._auto_config_button.set_sensitive(True)
self.update_epg_count()
self.show_info_message("{} {} {}".format(translate("Done!"),
translate("Count of successfully configured services:"),
success_count), Gtk.MessageType.INFO)
yield True
def assign_refs(self, model, paths, data):
[self.assign_data(model[p], data) for p in paths]
@@ -1209,7 +1344,7 @@ class EpgDialog:
fav_id = row[Column.FAV_ID]
fav_id_data = fav_id.split(":")
fav_id_data[3:7] = data[-3].split(":")
fav_id_data[3:7] = data[-3].split(":")[3:7]
if data[-2]:
row[Column.FAV_POS] = data[-2]
@@ -1365,6 +1500,7 @@ class EpgDialog:
self._url_to_xml_entry.set_text(epg_options.get("url_to_xml", ""))
self._enable_dat_filter = epg_options.get("enable_filtering", False)
self._enable_filtering_switch.set_active(self._enable_dat_filter)
self._enable_deep_comparing_switch.set_active(epg_options.get("enable_deep_comparing", False))
epg_dat_path = epg_options.get("epg_dat_path", epg_dat_path)
self._epg_dat_path_entry.set_text(epg_dat_path)
self._epg_dat_stb_path_entry.set_text(epg_options.get("epg_dat_stb_path", default_epg_data_stb_path))
@@ -1381,6 +1517,7 @@ class EpgDialog:
"local_path_to_xml": self._xml_chooser_button.get_filename(),
"url_to_xml": self._url_to_xml_entry.get_text(),
"enable_filtering": self._enable_filtering_switch.get_active(),
"enable_deep_comparing": self._enable_deep_comparing_switch.get_active(),
"epg_dat_path": self._epg_dat_path_entry.get_text(),
"epg_dat_stb_path": self._epg_dat_stb_path_entry.get_text(),
"epg_data_update_on_start": self._update_on_start_switch.get_active()}

View File

@@ -132,8 +132,8 @@ class ExtensionManager(Gtk.Window):
self._load_spinner.bind_property("active", self._view, "sensitive", GObject.BindingFlags.INVERT_BOOLEAN)
load_box.pack_end(self._load_spinner, False, False, 0)
status_box.pack_end(load_box, False, False, 0)
data_box.pack_end(status_box, False, True, 0)
scrolled = Gtk.ScrolledWindow(shadow_type=Gtk.ShadowType.IN)
scrolled.add(self._view)
data_box.pack_start(scrolled, True, True, 0)
@@ -142,27 +142,27 @@ class ExtensionManager(Gtk.Window):
self.add(main_box)
# Popup menu.
menu = Gtk.Menu()
item = Gtk.MenuItem.new_with_label(translate("Download"))
item.connect("activate", self.on_download)
menu.append(item)
item = Gtk.MenuItem.new_with_label(translate("Remove"))
item.connect("activate", self.on_remove)
menu.append(item)
download_menu_item = Gtk.MenuItem.new_with_label(translate("Download"))
download_menu_item.connect("activate", self.on_download)
menu.append(download_menu_item)
remove_menu_item = Gtk.MenuItem.new_with_label(translate("Remove"))
remove_menu_item.connect("activate", self.on_remove)
menu.append(remove_menu_item)
menu.show_all()
self._view.connect("button-press-event", self.on_view_popup_menu, menu)
# Header and toolbar.
download_button = Gtk.Button.new_from_icon_name("go-bottom-symbolic", Gtk.IconSize.BUTTON)
download_button.set_label(translate("Download"))
download_button.set_always_show_image(True)
download_button.connect("clicked", self.on_download)
self._download_button = Gtk.Button.new_from_icon_name("go-bottom-symbolic", Gtk.IconSize.BUTTON)
self._download_button.set_tooltip_text(translate("Download"))
self._download_button.set_always_show_image(True)
self._download_button.connect("clicked", self.on_download)
remove_button = Gtk.Button.new_from_icon_name("user-trash-symbolic", Gtk.IconSize.BUTTON)
remove_button.set_label(translate("Remove"))
remove_button.set_tooltip_text(translate("Remove"))
remove_button.set_always_show_image(True)
remove_button.connect("clicked", self.on_remove)
if app.app_settings.use_header_bar:
header = HeaderBar()
header.pack_start(download_button)
header.pack_start(self._download_button)
header.pack_start(remove_button)
self.set_titlebar(header)
@@ -170,14 +170,21 @@ class ExtensionManager(Gtk.Window):
else:
toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
toolbar.get_style_context().add_class("primary-toolbar")
button_box = Gtk.Box(spacing=2, orientation=Gtk.Orientation.HORIZONTAL, **margin)
button_box.pack_start(download_button, False, False, 0)
margin["margin_start"] = 15
margin["margin_top"] = 10
button_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL, **margin)
button_box.pack_start(self._download_button, False, False, 0)
button_box.pack_start(remove_button, False, False, 0)
toolbar.pack_start(button_box, True, True, 0)
main_box.pack_start(toolbar, False, False, 0)
main_box.pack_start(frame, True, True, 0)
main_box.show_all()
# Connection status.
self._connection_status_image = Gtk.Image.new_from_icon_name("network-offline-symbolic", Gtk.IconSize.BUTTON)
status_box.pack_end(self._connection_status_image, False, False, 0)
self._download_button.bind_property("visible", self._connection_status_image, "visible", 4)
self._download_button.bind_property("visible", download_menu_item, "visible")
ws_property = "extension_manager_window_size"
window_size = self._app.app_settings.get(ws_property, None)
@@ -225,20 +232,23 @@ class ExtensionManager(Gtk.Window):
@run_task
def update(self):
with requests.get(url=EXT_LIST_FILE, stream=True) as resp:
error_msg = None
if resp.status_code == 200:
try:
self.update_data(resp.json())
except ValueError as e:
error_msg = f"{self.__class__.__name__} [update] error: {e}"
else:
error_msg = f"{self.__class__.__name__} [update] error: {resp.reason}"
error_msg = None
try:
with requests.get(url=EXT_LIST_FILE, stream=True) as resp:
if resp.status_code == 200:
try:
self.update_data(resp.json())
except ValueError as e:
error_msg = f"{self.__class__.__name__} [update] error: {e}"
else:
error_msg = f"{self.__class__.__name__} [update] error: {resp.reason}"
GLib.idle_add(self._app.show_error_message, "Data loading error!")
except OSError as e:
error_msg = f"{self.__class__.__name__} [update] error: Connection error. {e}"
if error_msg:
log(error_msg)
GLib.idle_add(self._load_spinner.stop)
GLib.idle_add(self._app.show_error_message, "Data loading error!")
if error_msg:
log(error_msg)
self.update_local_data()
@run_idle
def update_data(self, data):
@@ -246,6 +256,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():

View File

@@ -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
@@ -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)

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2024 Dmitriy Yefremov
# Copyright (c) 2018-2025 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -97,7 +97,7 @@ def import_bouquet(app, model, path, appender, file_path=None):
def get_enigma2_bouquet(path):
p = Path(path)
bq = BouquetsReader.get_bouquet(f"{p.parent}{SEP}", f"{p.stem}{p.suffix}", p.stem)
bq = BouquetsReader().get_bouquet(f"{p.parent}{SEP}", f"{p.stem}{p.suffix}", p.stem)
bouquet = Bouquet(name=bq[0], type=BqType(p.suffix.lstrip(".")).value, services=bq[1], locked=None, hidden=None)
return bouquet
@@ -194,7 +194,11 @@ class ImportDialog:
try:
if not self._bouquets:
log("Import [init data]: getting bouquets...")
self._bouquets = get_bouquets(path, self._profile)
self._bouquets, errors = get_bouquets(path, self._profile)
if errors:
msg = translate('There were errors [%s] during bouquets loading!') % errors
self.show_info_message(f"{msg} {translate('Check the log for more info.')}",
Gtk.MessageType.WARNING)
for bqs in self._bouquets:
for bq in bqs.bouquets:
self._bq_model.append((bq.name, bq.type, True))

View File

@@ -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>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2024 Dmitriy Yefremov
# Copyright (c) 2018-2025 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -46,6 +46,7 @@ from app.eparser.iptv import (NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID
from app.settings import SettingsType
from app.tools.yt import YouTubeException, YouTube
from app.ui.dialogs import Action, show_dialog, DialogType, translate, get_builder, BaseDialog
from app.ui.epg.epg import EpgCache
from app.ui.main_helper import get_iptv_url, on_popup_menu, get_picon_pixbuf, show_info_bar_message, gen_bouquet_name
from app.ui.uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, IPTV_ICON, Column, KeyboardKey, get_yt_icon, HeaderBar)
@@ -769,6 +770,8 @@ class M3uImportDialog(IptvListDialog):
picons = {}
services = self._services
if self._app.app_settings.enable_epg_name_cache:
EpgCache.update_name_cache(self._app.app_settings.default_data_path, {s[3]: s[0] for s in services if s[0]})
if not self.is_all_data_default():
services = []

Binary file not shown.

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018-2024 Dmitriy Yefremov
Copyright (c) 2018-2025 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -32,7 +32,7 @@ Author: Dmitriy Yefremov
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellite list editor. -->
<!-- interface-copyright 2018-2024 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2025 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="alt_list_store">
<columns>
@@ -57,12 +57,23 @@ Author: Dmitriy Yefremov
<object class="GtkMenu" id="alt_popup_menu">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkMenuItem" id="alt_paste_popup_item">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Paste</property>
<signal name="activate" handler="on_alt_paste" swapped="no"/>
<accelerator key="v" signal="activate" modifiers="Primary"/>
</object>
</child>
<child>
<object class="GtkMenuItem" id="alt_remove_popup_item">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Remove</property>
<signal name="activate" handler="on_delete" object="alt_tree_view" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
</child>
</object>
@@ -298,6 +309,67 @@ Author: Dmitriy Yefremov
</object>
</child>
</object>
<object class="GtkPopover" id="filter_cas_popover">
<property name="can-focus">False</property>
<child>
<object class="GtkButtonBox">
<property name="width-request">100</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">5</property>
<property name="orientation">vertical</property>
<property name="layout-style">expand</property>
<child>
<object class="GtkRadioButton" id="filter_all_button">
<property name="label" translatable="yes">All</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="active">True</property>
<property name="draw-indicator">False</property>
<signal name="toggled" handler="on_filter_changed" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="filter_free_button">
<property name="label" translatable="yes">Free (FTA)</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">False</property>
<property name="group">filter_coded_button</property>
<signal name="toggled" handler="on_filter_changed" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="filter_coded_button">
<property name="label" translatable="yes">Coded</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">False</property>
<property name="group">filter_all_button</property>
<signal name="toggled" handler="on_filter_changed" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkListStore" id="filter_sat_pos_list_store">
<columns>
<!-- column-name satellite -->
@@ -1130,7 +1202,7 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_popup_separator">
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_4">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
@@ -1145,7 +1217,31 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_4">
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_5">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
</child>
<child>
<object class="GtkMenuItem" id="fav_use_sr_popup_item">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Use with Streamrelay</property>
<signal name="activate" handler="on_use_streamrelay" swapped="no"/>
</object>
</child>
<child>
<object class="GtkMenuItem" id="fav_remove_sr_popup_item">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Remove use with Streamrelay</property>
<signal name="activate" handler="on_remove_use_streamrelay" swapped="no"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_6">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
@@ -1172,7 +1268,7 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_5">
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_7">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
@@ -1194,7 +1290,7 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_6">
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_8">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
@@ -1217,7 +1313,7 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_7">
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_9">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
@@ -1231,7 +1327,7 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_8">
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_10">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
@@ -1249,7 +1345,7 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_9">
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_11">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
@@ -1292,7 +1388,7 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_pupup_separator_10">
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_12">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
@@ -1310,7 +1406,7 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_11">
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_13">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
@@ -1455,9 +1551,42 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkMenuItem" id="services_add_new_popup_item">
<property name="can-focus">False</property>
<property name="label" translatable="yes">New</property>
<signal name="activate" handler="on_services_add_new" swapped="no"/>
<property name="visible">True</property>
<property name="can-focus">False</property>
<child type="submenu">
<object class="GtkMenu" id="et_popup_menu">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkMenuItem">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Satellite channel</property>
<property name="name">s</property>
<signal name="activate" handler="on_services_add_new" swapped="no"/>
</object>
</child>
<child>
<object class="GtkMenuItem">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Terrestrial channel</property>
<property name="name">t</property>
<signal name="activate" handler="on_services_add_new" swapped="no"/>
</object>
</child>
<child>
<object class="GtkMenuItem">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Cable channel</property>
<property name="name">c</property>
<signal name="activate" handler="on_services_add_new" swapped="no"/>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
@@ -1572,6 +1701,7 @@ Author: Dmitriy Yefremov
</object>
<object class="GtkComboBoxText" id="profile_combo_box">
<property name="visible">True</property>
<property name="sensitive" bind-source="iptv_progress_bar" bind-property="visible" bind-flags="invert-boolean">True</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="active">0</property>
@@ -1722,7 +1852,7 @@ Author: Dmitriy Yefremov
<object class="GtkLabel" id="app_ver_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">3.9.0 Beta</property>
<property name="label">3.14.1 Beta</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
@@ -1776,6 +1906,7 @@ Author: Dmitriy Yefremov
<property name="label" translatable="yes">DVB</property>
<property name="width-request">80</property>
<property name="visible">True</property>
<property name="sensitive" bind-source="iptv_progress_bar" bind-property="visible" bind-flags="invert-boolean">True</property>
<property name="can-focus">False</property>
<property name="receives-default">False</property>
<property name="active">True</property>
@@ -1792,6 +1923,7 @@ Author: Dmitriy Yefremov
<object class="GtkRadioButton" id="iptv_button">
<property name="label" translatable="yes">IPTV</property>
<property name="visible">True</property>
<property name="sensitive" bind-source="services_progress_bar" bind-property="visible" bind-flags="invert-boolean">True</property>
<property name="can-focus">False</property>
<property name="receives-default">False</property>
<property name="active">True</property>
@@ -1815,7 +1947,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkToggleButton" id="filter_services_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="sensitive" bind-source="services_progress_bar" bind-property="visible" bind-flags="invert-boolean">False</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
@@ -1837,6 +1969,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkToggleButton" id="filter_iptv_services_button">
<property name="sensitive" bind-source="iptv_progress_bar" bind-property="visible" bind-flags="invert-boolean">False</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
@@ -1998,22 +2131,23 @@ Author: Dmitriy Yefremov
<object class="GtkBox" id="services_fs_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-left">5</property>
<property name="margin-right">5</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-bottom">5</property>
<property name="spacing">10</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="filter_box">
<property name="can-focus">False</property>
<property name="valign">center</property>
<child>
<object class="GtkToggleButton" id="filter_only_free_button">
<object class="GtkMenuButton" id="filter_cas_menu_button">
<property name="width-request">50</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Only free</property>
<signal name="toggled" handler="on_filter_changed" swapped="no"/>
<property name="tooltip-text" translatable="yes">Access</property>
<property name="popover">filter_cas_popover</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
@@ -2595,8 +2729,8 @@ Author: Dmitriy Yefremov
<property name="height-request">26</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-left">5</property>
<property name="margin-right">5</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="resize-mode">queue</property>
<property name="spacing">2</property>
<child>
@@ -2729,12 +2863,13 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkSpinner" id="services_load_spinner">
<property name="visible">True</property>
<object class="GtkProgressBar" id="services_progress_bar">
<property name="visible">False</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Loading data...</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin-start">15</property>
</object>
<packing>
<property name="expand">False</property>
@@ -2743,6 +2878,37 @@ Author: Dmitriy Yefremov
<property name="position">10</property>
</packing>
</child>
<child>
<object class="GtkImage" id="current_data_path_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Current data path</property>
<property name="icon-name">document-open-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">11</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="current_data_path_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Current data path</property>
<property name="label"> </property>
<property name="ellipsize">end</property>
<property name="xalign">0</property>
<attributes>
<attribute name="size" value="8000"/>
</attributes>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">12</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -3077,8 +3243,8 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkSpinner" id="iptv_services_load_spinner">
<property name="visible">True</property>
<object class="GtkProgressBar" id="iptv_progress_bar">
<property name="visible">False</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Loading data...</property>
<property name="halign">center</property>
@@ -3706,6 +3872,8 @@ Author: Dmitriy Yefremov
<signal name="drag-begin" handler="on_view_drag_begin" after="yes" swapped="no"/>
<signal name="drag-data-get" handler="on_view_drag_data_get" swapped="no"/>
<signal name="drag-data-received" handler="on_alt_view_drag_data_received" swapped="no"/>
<signal name="focus-in-event" handler="on_view_focus" swapped="no"/>
<signal name="key-press-event" handler="on_alt_view_key_press" swapped="no"/>
<signal name="row-activated" handler="on_alt_selection" object="alt_list_store" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
@@ -4638,97 +4806,185 @@ Author: Dmitriy Yefremov
<property name="can-focus">False</property>
<property name="stack">stack</property>
</object>
<object class="GtkButtonBox" id="toolbar_main_box">
<object class="GtkPopover" id="send_popover">
<property name="can-focus">False</property>
<child>
<object class="GtkButtonBox">
<property name="width-request">100</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">5</property>
<property name="orientation">vertical</property>
<property name="homogeneous">True</property>
<property name="layout-style">expand</property>
<child>
<object class="GtkModelButton" id="send_srv_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="action-name">app.on_send</property>
<property name="text" translatable="yes">All</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="send_bq_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="action-name">app.upload_bouquets</property>
<property name="text" translatable="yes">Bouquets</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkBox" id="toolbar_main_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="homogeneous">True</property>
<property name="layout-style">expand</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="open_tool_button">
<object class="GtkButtonBox" id="osv_button_box">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Open</property>
<property name="action-name">app.on_data_open</property>
<property name="can-focus">False</property>
<property name="layout-style">expand</property>
<child>
<object class="GtkImage" id="open_image">
<object class="GtkButton" id="open_tool_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-open-symbolic</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Open</property>
<property name="action-name">app.on_data_open</property>
<child>
<object class="GtkImage" id="open_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-open-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="save_tool_button">
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Save</property>
<property name="action-name">app.on_data_save</property>
<child>
<object class="GtkImage" id="save_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-save-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkButton" id="receive_button">
<object class="GtkButtonBox" id="send_receive_button_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Download from the receiver</property>
<property name="action-name">app.on_receive</property>
<property name="always-show-image">True</property>
<property name="layout-style">expand</property>
<child>
<object class="GtkImage" id="download_image">
<property name="width-request">32</property>
<object class="GtkButton" id="receive_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">network-receive-symbolic</property>
<property name="icon_size">1</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Download from the receiver</property>
<property name="action-name">app.on_receive</property>
<property name="always-show-image">True</property>
<child>
<object class="GtkImage" id="download_image">
<property name="width-request">32</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">network-receive-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="send_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Transfer to receiver</property>
<property name="action-name">app.on_send</property>
<property name="always-show-image">True</property>
<child>
<object class="GtkImage" id="send_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">network-transmit-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="send_menu_button">
<property name="visible" bind-source="send_button" bind-property="visible" bind-flags="invert-boolean">False</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Transfer to receiver</property>
<property name="popover">send_popover</property>
<child>
<object class="GtkImage" id="send_menu_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">network-transmit-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="send_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Transfer to receiver</property>
<property name="action-name">app.on_send</property>
<property name="always-show-image">True</property>
<child>
<object class="GtkImage" id="send_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">network-transmit-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="save_tool_button">
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Save</property>
<property name="action-name">app.on_data_save</property>
<child>
<object class="GtkImage" id="save_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-save-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
<property name="position">5</property>
</packing>
</child>
</object>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2024 Dmitriy Yefremov
# Copyright (c) 2018-2025 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -40,16 +40,18 @@ from urllib.parse import urlparse, unquote
from gi.repository import GLib, Gio, GObject
from app.commons import run_idle, log, run_task, run_with_delay, init_logger, DefaultDict
from app.connections import (HttpAPI, download_data, DownloadType, upload_data, STC_XML_FILE)
from app.connections import (HttpAPI, download_data, DownloadType, upload_data)
from app.eparser import get_blacklist, write_blacklist, write_bouquet
from app.eparser import get_services, get_bouquets, write_bouquets, write_services, Bouquets, Bouquet, Service
from app.eparser.ecommons import CAS, Flag, BouquetService
from app.eparser.ecommons import CAS, Flag, BouquetService, TrType
from app.eparser.enigma.bouquets import BqServiceType
from app.eparser.enigma.streamrelay import StreamRelay
from app.eparser.iptv import export_to_m3u, StreamType
from app.eparser.neutrino.bouquets import BqType
from app.settings import (SettingsType, Settings, SettingsException, SettingsReadException, IS_DARWIN, IS_LINUX,
PlayStreamsMode, PlaybackMode, USE_HEADER_BAR)
from app.tools.media import Recorder
from app.ui.bootlogo import BootLogoManager
from app.ui.control import ControlTool
from app.ui.epg.epg import FavEpgCache, EpgSettingsPopover, EpgDialog, EpgTool
from app.ui.ftp import FtpClientBox
@@ -70,13 +72,15 @@ from .search import SearchProvider
from .service_details_dialog import ServiceDetailsDialog, Action
from .settings_dialog import SettingsDialog
from .uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON, IPTV_ICON, MOVE_KEYS, KeyboardKey, Column,
MOD_MASK, APP_FONT, Page, HeaderBar)
MOD_MASK, APP_FONT, Page, HeaderBar, LINK_ICON)
from .xml.dialogs import ServicesUpdateDialog
from .xml.edit import SatellitesTool
class Application(Gtk.Application):
""" Main application class. """
VERSION = "3.14.1"
SERVICE_MODEL = "services_list_store"
FAV_MODEL = "fav_list_store"
BQ_MODEL = "bouquets_tree_store"
@@ -98,13 +102,15 @@ class Application(Gtk.Application):
# Dynamically active elements depending on the selected view
_SERVICE_ELEMENTS = ("services_to_fav_end_move_popup_item", "services_to_fav_move_popup_item",
"services_create_bouquet_popup_item", "services_copy_popup_item", "services_edit_popup_item",
"services_add_new_popup_item", "services_picon_popup_item", "services_remove_popup_item")
"services_mark_not_in_bq_popup_item", "services_clear_not_in_bq_popup_item",
"services_clear_new_flag_item", "services_picon_popup_item", "services_remove_popup_item",
"services_reference_popup_item")
_FAV_ELEMENTS = ("fav_cut_popup_item", "fav_paste_popup_item", "fav_locate_popup_item", "fav_iptv_popup_item",
"fav_insert_marker_popup_item", "fav_insert_space_popup_item", "fav_edit_sub_menu_popup_item",
"fav_edit_popup_item", "fav_picon_popup_item", "fav_copy_popup_item", "fav_add_alt_popup_item",
"fav_epg_configuration_popup_item", "fav_mark_dup_popup_item", "fav_remove_dup_popup_item",
"fav_reference_popup_item")
"fav_reference_popup_item", "fav_use_sr_popup_item", "fav_remove_sr_popup_item")
_BOUQUET_ELEMENTS = ("bouquets_new_popup_item", "bouquets_edit_popup_item", "bouquets_cut_popup_item",
"bouquets_copy_popup_item", "bouquets_paste_popup_item", "new_header_button",
@@ -115,6 +121,7 @@ class Application(Gtk.Application):
_FAV_ENIGMA_ELEMENTS = ("fav_insert_marker_popup_item", "fav_epg_configuration_popup_item")
_FAV_IPTV_ELEMENTS = ("fav_iptv_popup_item", "iptv_menu_button")
_ALT_ELEMENTS = ("alt_paste_popup_item",)
DATA_SAVE_PAGES = {Page.SERVICES, Page.SATELLITE}
DATA_OPEN_PAGES = {Page.SERVICES, Page.SATELLITE, Page.PICONS, Page.EPG}
@@ -148,6 +155,7 @@ class Application(Gtk.Application):
"on_reference_assign": self.on_reference_assign,
"on_fav_paste": self.on_fav_paste,
"on_bouquets_paste": self.on_bouquets_paste,
"on_alt_paste": self.on_alt_paste,
"on_rename_for_bouquet": self.on_rename_for_bouquet,
"on_set_default_name_for_bouquet": self.on_set_default_name_for_bouquet,
"on_services_add_new": self.on_services_add_new,
@@ -168,6 +176,7 @@ class Application(Gtk.Application):
"on_view_drag_data_received": self.on_view_drag_data_received,
"on_bq_view_drag_data_received": self.on_bq_view_drag_data_received,
"on_alt_view_drag_data_received": self.on_alt_view_drag_data_received,
"on_alt_view_key_press": self.on_alt_view_key_press,
"on_view_press": self.on_view_press,
"on_view_release": self.on_view_release,
"on_view_popup_menu": self.on_view_popup_menu,
@@ -217,6 +226,8 @@ class Application(Gtk.Application):
"on_create_bouquet_for_current_type": self.on_create_bouquet_for_current_type,
"on_create_bouquet_for_each_type": self.on_create_bouquet_for_each_type,
"on_add_alternatives": self.on_add_alternatives,
"on_use_streamrelay": self.on_use_streamrelay,
"on_remove_use_streamrelay": self.on_remove_use_streamrelay,
"on_satellites_realize": self.on_satellites_realize,
"on_picons_realize": self.on_picons_realize,
"on_epg_realize": self.on_epg_realize,
@@ -237,6 +248,8 @@ class Application(Gtk.Application):
self._is_data_open_enabled = True
self._is_data_extract_enabled = False
self._is_data_save_enabled = False
self._ext_data_path = None
self._arch_data_path = None
# Used for copy/paste. When adding the previous data will not be deleted.
# Clearing only after the insertion!
self._rows_buffer = []
@@ -253,6 +266,7 @@ class Application(Gtk.Application):
# For bouquets with different names of services in bouquet and main list
self._extra_bouquets = {}
self._blacklist = set()
self._stream_relay = StreamRelay()
self._current_bq_name = None
self._bq_selected = "" # Current selected bouquet
self._select_enabled = True # Multiple selection
@@ -293,8 +307,18 @@ class Application(Gtk.Application):
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("bouquet-changed", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("bouquet-added", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("bouquet-remove", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("bouquet-removed", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("fav-changed", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("fav-added", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("fav-removed", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("fav-clicked", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("srv-clicked", self, GObject.SIGNAL_RUN_LAST,
@@ -375,6 +399,7 @@ class Application(Gtk.Application):
self._bq_name_label = builder.get_object("bq_name_label")
self._iptv_model = builder.get_object("iptv_list_store")
self._iptv_menu_button = builder.get_object("iptv_menu_button")
self._send_button = builder.get_object("send_button")
# Setting custom sort function for position column.
self._services_view.get_model().set_sort_func(Column.SRV_POS, self.position_sort_func, Column.SRV_POS)
# App info
@@ -400,9 +425,10 @@ class Application(Gtk.Application):
self._tv_count_label = builder.get_object("tv_count_label")
self._radio_count_label = builder.get_object("radio_count_label")
self._data_count_label = builder.get_object("data_count_label")
self._current_data_path_label = builder.get_object("current_data_path_label")
self._iptv_count_label = builder.get_object("iptv_count_label")
self._services_load_spinner = builder.get_object("services_load_spinner")
self._iptv_services_load_spinner = builder.get_object("iptv_services_load_spinner")
self._services_progress_bar = builder.get_object("services_progress_bar")
self._iptv_progress_bar = builder.get_object("iptv_progress_bar")
self._save_tool_button = builder.get_object("save_tool_button")
self.bind_property("is-data-save-enabled", self._save_tool_button, "visible")
self._receiver_info_box.bind_property("visible", self._http_status_image, "visible", 4)
@@ -439,9 +465,10 @@ class Application(Gtk.Application):
self._filter_types_model = builder.get_object("filter_types_list_store")
self._filter_sat_pos_model = builder.get_object("filter_sat_pos_list_store")
self._filter_bouquet_model = builder.get_object("filter_bouquet_list_store")
self._filter_only_free_button = builder.get_object("filter_only_free_button")
self._filter_all_button = builder.get_object("filter_all_button")
self._filter_free_button = builder.get_object("filter_free_button")
self._filter_coded_button = builder.get_object("filter_coded_button")
self._filter_not_in_bq_button = builder.get_object("filter_not_in_bq_button")
self._services_load_spinner.bind_property("active", self._filter_services_button, "sensitive", 4)
self._filter_iptv_services_button = builder.get_object("filter_iptv_services_button")
# Search.
services_search_provider = SearchProvider(self._services_view,
@@ -481,7 +508,7 @@ class Application(Gtk.Application):
self._record_image = builder.get_object("record_button_image")
# Dynamically active elements depending on the selected view.
d_elements = (self._SERVICE_ELEMENTS, self._BOUQUET_ELEMENTS, self._COMMONS_ELEMENTS, self._FAV_ELEMENTS,
self._FAV_ENIGMA_ELEMENTS, self._FAV_IPTV_ELEMENTS)
self._FAV_ENIGMA_ELEMENTS, self._FAV_IPTV_ELEMENTS, self._ALT_ELEMENTS)
self._tool_elements = {k: builder.get_object(k) for k in set(chain.from_iterable(d_elements))}
# Lock, Hide.
self._bouquet_lock_hide_box = builder.get_object("bouquet_lock_hide_box")
@@ -576,10 +603,6 @@ class Application(Gtk.Application):
iptv_button.bind_property("active", self._filter_iptv_services_button, "visible")
iptv_button.bind_property("active", self._iptv_search_button, "visible")
iptv_button.bind_property("active", builder.get_object("iptv_export_to_m3u_button"), "visible")
self._iptv_services_load_spinner.bind_property("active", self._filter_iptv_services_button, "sensitive", 4)
self._iptv_services_load_spinner.bind_property("active", self._profile_combo_box, "sensitive", 4)
self._iptv_services_load_spinner.bind_property("active", self._dvb_button, "sensitive", 4)
self._services_load_spinner.bind_property("active", self._iptv_button, "sensitive", 4)
self.connect("profile-changed", self.init_iptv)
self.connect("iptv-service-added", self.on_iptv_service_added)
self.connect("iptv-service-edited", self.on_iptv_service_edited)
@@ -623,7 +646,7 @@ class Application(Gtk.Application):
self._main_window.set_wmclass("DemonEditor", "DemonEditor")
self._main_window.present()
self.init_profiles()
self.init_profiles(True)
gen = self.init_http_api()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
@@ -768,9 +791,12 @@ class Application(Gtk.Application):
self.set_action("on_import_bouquet", self.on_import_bouquet)
self.set_action("on_import_bouquets", self.on_import_bouquets)
self.set_action("on_new_configuration", self.on_new_configuration)
self.set_action("on_import_from_web", self.on_import_from_web)
sa = self.set_action("on_import_from_web", self.on_import_from_web)
self.bind_property("is-data-save-enabled", sa, "enabled")
# Tools.
self.set_action("on_backup_tool_show", self.on_backup_tool_show)
sa = self.set_action("on_boot_logo_tool_show", self.on_boot_logo_tool_show)
self.bind_property("is-enigma", sa, "enabled")
self.set_state_action("on_telnet_show", self.on_telnet_show, False)
self.set_state_action("on_logs_show", self.on_logs_show, False)
# Filter.
@@ -784,8 +810,8 @@ class Application(Gtk.Application):
self.set_action("on_locked", self.on_locked)
# Open and download/upload data.
self.set_action("open_data", lambda a, v: self.open_data())
self.set_action("upload_all", lambda a, v: self.emit("data-send", self._page))
self.set_action("upload_bouquets", lambda a, v: self.on_upload_data(DownloadType.BOUQUETS))
self.set_action("upload_all", lambda a, v: self.emit("data-send", self._page))
self.set_action("upload_bouquets", self.on_upload_bouquets)
sa = self.set_action("on_data_save", lambda a, v: self.emit("data-save", self._page), False)
self.bind_property("is-data-save-enabled", sa, "enabled")
sa = self.set_action("on_data_save_as", lambda a, v: self.emit("data-save-as", self._page), False)
@@ -875,9 +901,9 @@ class Application(Gtk.Application):
self.set_accels_for_action("app.on_logs_show", ["<shift><primary>l"])
self.set_accels_for_action("win.filter", ["<shift><primary>f"])
def init_profiles(self):
def init_profiles(self, last_config=False):
self.update_profiles()
if self._settings.load_last_config:
if last_config and self._settings.load_last_config:
config = self._settings.get("last_config") or {}
if config.get("last_bouquet", None):
self.connect("data-load-done", self.open_last_bouquet)
@@ -1146,6 +1172,7 @@ class Application(Gtk.Application):
def on_visible_page(self, stack, param):
self._page = Page(stack.get_visible_child_name())
self._send_button.set_visible(self._page is not Page.SERVICES)
self._fav_paned.set_visible(self._page in self._fav_pages)
self.is_data_save_enabled = self._page in self.DATA_SAVE_PAGES
self.is_data_open_enabled = self._page in self.DATA_OPEN_PAGES
@@ -1170,7 +1197,10 @@ class Application(Gtk.Application):
self._stack.set_visible_child_name(page_name)
def on_page_changed(self, app, page):
if not self._settings.load_last_config and not len(self._bouquets_model):
if page is not Page.SERVICES:
return
if all((not self._settings.load_last_config, not self.is_data_loading(), not len(self._bouquets_model))):
self.open_data()
def set_use_alt_layout(self, action, value):
@@ -1355,6 +1385,8 @@ class Application(Gtk.Application):
self.fav_paste(selection)
elif target is ViewTarget.BOUQUET:
self.bouquet_paste(selection)
elif target is ViewTarget.ALT:
self.alt_paste()
self.on_view_focus(view)
def fav_paste(self, selection):
@@ -1378,6 +1410,7 @@ class Application(Gtk.Application):
self.update_fav_num_column(model)
self._rows_buffer.clear()
self.emit("fav-added", self._bq_selected)
def bouquet_paste(self, selection):
model, paths = selection.get_selected_rows()
@@ -1467,9 +1500,11 @@ class Application(Gtk.Application):
if index % self.DEL_FACTOR == 0:
yield True
self.update_fav_num_column(model)
self.emit("fav-removed", self._bq_selected)
self.on_model_changed(self._fav_model)
self._wait_dialog.hide()
yield True
def delete_services(self, itrs, model, rows, srv_model, fav_column=Column.SRV_FAV_ID):
@@ -1511,6 +1546,8 @@ class Application(Gtk.Application):
self.show_error_message("This item is not allowed to be removed!")
return
self.emit("bouquet-remove", self._bouquets)
for itr in itrs:
if len(model.get_path(itr)) < 2:
continue
@@ -1525,6 +1562,7 @@ class Application(Gtk.Application):
self._bq_name_label.set_text(self._bq_selected)
self.on_model_changed(model)
self._wait_dialog.hide()
self.emit("bouquet-removed", self._bouquets)
yield True
# ***************** Bouquets ********************* #
@@ -1583,7 +1621,9 @@ class Application(Gtk.Application):
else:
it = model.insert(p_itr, int(model.get_path(itr)[1]) + 1, bq) if p_itr else model.append(itr, bq)
scroll_to(model.get_path(it), view, paths)
self._bouquets[key] = []
self.emit("bouquet-added", key)
def on_new_sub_bouquet(self, item=None):
self.on_new_bouquet(self._bouquets_view, True)
@@ -2076,6 +2116,8 @@ class Application(Gtk.Application):
ch.fav_id, self._picons.get(ch.picon_id, None), None, None))
fav_bouquet.insert(dst_index, ch.fav_id)
self.emit("fav-added", self._bq_selected)
def on_view_press(self, view, event):
""" Handles a mouse click (press) to view. """
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_PRIMARY:
@@ -2122,6 +2164,8 @@ class Application(Gtk.Application):
elif name == "bouquets_popup_menu":
self.delete_selection(self._services_view, self._fav_view)
self.on_view_focus(self._bouquets_view)
elif name == "alt_popup_menu":
self.on_view_focus(self._alt_view)
menu.popup(None, None, None, None, event.button, event.time)
return True
@@ -2143,12 +2187,20 @@ class Application(Gtk.Application):
self.show_error_message("Not allowed in this context!")
def on_download(self, app, page):
if page is Page.SERVICES or page is Page.INFO:
if page is Page.INFO:
self.on_download_data()
elif page is Page.SERVICES:
self.on_download_data(DownloadType.SERVICES)
def on_upload(self, app, page):
if page is Page.SERVICES or page is Page.INFO:
if page is Page.INFO:
self.on_upload_data()
elif page is Page.SERVICES:
self.on_upload_data(DownloadType.SERVICES)
def on_upload_bouquets(self, action, value=None):
self.change_action_state("on_logs_show", GLib.Variant.new_boolean(True))
self.on_upload_data(DownloadType.BOUQUETS)
def on_bg_task_add(self, app, task):
if len(self._task_box) <= self.BG_TASK_LIMIT:
@@ -2166,7 +2218,7 @@ class Application(Gtk.Application):
self.on_task_done(app, task)
@run_task
def on_download_data(self, download_type=DownloadType.ALL):
def on_download_data(self, download_type=DownloadType.ALL, files_filter=None):
backup, backup_src, data_path = self._settings.backup_before_downloading, None, None
try:
if backup and download_type is not DownloadType.SATELLITES:
@@ -2174,7 +2226,7 @@ class Application(Gtk.Application):
backup_path = self._settings.profile_backup_path or self._settings.default_backup_path
backup_src = backup_data(data_path, backup_path, download_type is DownloadType.ALL)
download_data(settings=self._settings, download_type=download_type)
download_data(settings=self._settings, download_type=download_type, files_filter=files_filter)
except Exception as e:
msg = "Downloading data error: {}"
log(msg.format(e), debug=self._settings.debug_mode, fmt_message=msg)
@@ -2187,22 +2239,26 @@ class Application(Gtk.Application):
else:
GLib.idle_add(self.open_data)
def on_upload_data(self, download_type=DownloadType.ALL):
def on_upload_data(self, download_type=DownloadType.ALL, files_filter=None):
if not self.is_data_saved():
gen = self.save_data(lambda: self.upload_data(download_type))
gen = self.save_data(lambda: self.upload_data(download_type, files_filter=files_filter))
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
else:
self.upload_data(download_type)
self.upload_data(download_type, files_filter=files_filter)
@run_task
def upload_data(self, download_type):
def upload_data(self, download_type, files_filter=None):
opts = self._settings
multiple = len(self._settings.hosts) > 1
for host in self._settings.hosts:
if multiple:
log(f"##### Uploading data on [{host}] #####")
try:
upload_data(settings=opts, download_type=download_type, ext_host=host)
upload_data(settings=opts,
download_type=download_type,
files_filter=files_filter,
ext_host=host,
ext_path=self._ext_data_path)
except Exception as e:
msg = "Uploading data error: {}"
log(msg.format(e), debug=self._settings.debug_mode, fmt_message=msg)
@@ -2216,11 +2272,12 @@ class Application(Gtk.Application):
response = show_dialog(DialogType.CHOOSER, self._main_window, settings=self._settings, title="Open folder")
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
self.open_data(response)
def on_data_extract(self, app, page):
""" Opening the data archive via "File/Extract...". """
if page is Page.SERVICES:
if page is Page.INFO or page is Page.SERVICES:
file_filter = None
if IS_DARWIN:
file_filter = Gtk.FileFilter()
@@ -2248,9 +2305,9 @@ class Application(Gtk.Application):
def open_compressed_data(self, data_path):
""" Opening archived data. """
arch_path = self.get_archive_path(data_path)
if arch_path:
gen = self.update_data(f"{arch_path.name}{os.sep}", callback=arch_path.cleanup)
self._arch_data_path = self.get_archive_path(data_path)
if self._arch_data_path:
gen = self.update_data(f"{self._arch_data_path.name}{os.sep}")
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def get_archive_path(self, data_path):
@@ -2285,10 +2342,13 @@ class Application(Gtk.Application):
return tmp_path
def update_data(self, data_path, callback=None):
self._ext_data_path = data_path
self.on_info_bar_close()
self._profile_combo_box.set_sensitive(False)
self._alt_revealer.set_visible(False)
self._filter_services_button.set_active(False)
self._wait_dialog.show()
self._services_progress_bar.show()
yield from self.clear_current_data()
# Reset of sorting
@@ -2306,20 +2366,17 @@ class Application(Gtk.Application):
self.init_profiles()
data_path = self._settings.profile_data_path if data_path is None else data_path
self._current_data_path_label.set_text(data_path)
local_path = self._settings.profile_data_path
os.makedirs(os.path.dirname(local_path), exist_ok=True)
if data_path != local_path:
from shutil import copyfile
for f in STC_XML_FILE:
xml_src = data_path + f
if os.path.isfile(xml_src):
copyfile(xml_src, local_path + f)
prf = self._s_type
black_list = get_blacklist(data_path)
bouquets = get_bouquets(data_path, prf)
self._stream_relay.refresh(data_path)
bouquets, errors = get_bouquets(data_path, prf)
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)
yield True
services = get_services(data_path, prf, self.get_format_version() if prf is SettingsType.ENIGMA_2 else 0)
yield True
@@ -2337,7 +2394,8 @@ class Application(Gtk.Application):
return
else:
self.append_blacklist(black_list)
yield from self.append_data(bouquets, services)
data_gen = self.append_data(bouquets, services)
yield from data_gen
if callback:
callback()
yield True
@@ -2349,13 +2407,15 @@ class Application(Gtk.Application):
finally:
self._profile_combo_box.set_sensitive(True)
self._wait_dialog.hide()
self._services_progress_bar.hide()
self.emit("data-load-done", self._settings.current_profile)
def append_data(self, bouquets, services):
if self._app_info_box.get_visible():
yield from self.show_app_info(False)
self.append_bouquets(bouquets)
yield from self.append_services(services)
s_gen = self.append_services(services)
yield from s_gen
self.update_sat_positions()
yield True
@@ -2414,35 +2474,27 @@ class Application(Gtk.Application):
bouquet = self._bouquets_model.append(parent, [name, locked, hidden, bq_type])
bq_id = f"{name}:{bq_type}"
services = []
extra_services = {} # for services with different names in bouquet and main list
extra_services = {} # For services with different names in the bouquet and main list.
agr = [None] * 7
for srv in bq.services:
fav_id = srv.data
# IPTV and MARKER services
s_type = srv.type
if s_type in (BqServiceType.MARKER, BqServiceType.IPTV, BqServiceType.SPACE):
icon = None
picon_id = None
data_id = srv.num
locked = None
if s_type is BqServiceType.IPTV:
icon = IPTV_ICON
fav_id_data = fav_id.lstrip().split(":")
if len(fav_id_data) > 10:
data_id = ":".join(fav_id_data[:11])
picon_id = "{}_{}_{}_{}_{}_{}_{}_{}_{}_{}.png".format(*fav_id_data[:10])
locked = LOCKED_ICON if data_id in self._blacklist else None
srv = Service(None, None, icon, srv.name, locked, None, None, s_type.name,
self._picons.get(picon_id, None), picon_id, *agr, data_id, fav_id, None)
self._services[fav_id] = srv
# Markers and spaces.
if s_type in (BqServiceType.MARKER, BqServiceType.SPACE):
self._services[fav_id] = Service(None, None, None, srv.name, None, None, None,
s_type.name, None, None, *agr, srv.num, fav_id, None)
# IPTV services.
elif s_type is BqServiceType.IPTV:
self._services[fav_id] = self.get_bq_iptv_service(srv, agr)
# Alternatives.
elif s_type is BqServiceType.ALT:
self._alt_file.add(f"{srv.data}:{bq_type}")
srv = Service(None, None, None, srv.name, locked, None, None, s_type.name,
None, None, *agr, srv.data, fav_id, srv.num)
self._services[fav_id] = srv
self._services[fav_id] = Service(None, None, None, srv.name, locked, None, None, s_type.name,
None, None, *agr, srv.data, fav_id, srv.num)
for s in filter(lambda x: x.type is BqServiceType.IPTV, srv[-1]):
iptv_srv = self.get_bq_iptv_service(s, agr)
self._services[iptv_srv.fav_id] = iptv_srv
elif s_type is BqServiceType.BOUQUET:
# Sub bouquets!
self.append_bouquet(srv.data, bouquet)
elif srv.name:
extra_services[fav_id] = srv.name
@@ -2453,6 +2505,24 @@ class Application(Gtk.Application):
if extra_services:
self._extra_bouquets[bq_id] = extra_services
def get_bq_iptv_service(self, srv, agr):
fav_id = srv.data
data_id = srv.num
picon_id = None
icon = None
locked = None
fav_id_data = fav_id.lstrip().split(":")
if len(fav_id_data) > 10:
data_id = ":".join(fav_id_data[:11])
picon_id = "{}_{}_{}_{}_{}_{}_{}_{}_{}_{}.png".format(*fav_id_data[:10])
icon = LINK_ICON if data_id in self._stream_relay else IPTV_ICON
locked = LOCKED_ICON if data_id in self._blacklist else None
return Service(None, None, icon, srv.name, locked, None, None, srv.type.name,
self._picons.get(picon_id, None), picon_id, *agr, data_id, fav_id, None)
@run_idle
def open_last_bouquet(self, app, profile):
""" Loads the last opened bouquet. """
@@ -2475,34 +2545,38 @@ class Application(Gtk.Application):
to_add.append(srv)
# Adding channels to dict with fav_id as keys.
self._services[srv.fav_id] = srv
self.update_services_counts(len(self._services.values()))
self._wait_dialog.hide()
self._services_load_spinner.start()
factor = self.DEL_FACTOR / 4
size = len(to_add)
for index, srv in enumerate(to_add):
background = self.get_new_background(srv.flags_cas)
s = srv + (None, background)
self._services_model.append(s)
if index % factor == 0:
self._services_progress_bar.set_fraction(index / size)
yield True
self._services_load_spinner.stop()
yield True
def append_iptv_data(self, services=None):
self._iptv_services_load_spinner.start()
self._iptv_progress_bar.show()
services = services or self._services.values()
services = [s for s in services if s.service_type == BqServiceType.IPTV.name]
size = len(services)
for index, s in enumerate(filter(lambda x: x.service_type == BqServiceType.IPTV.name, services), start=1):
for index, s in enumerate(services, start=1):
ref, url = get_iptv_data(s.fav_id)
self._iptv_model.append((s.service, None, None, ref, url, s.fav_id, s.picon_id, None))
if index % self.DEL_FACTOR == 0:
self._iptv_count_label.set_text(str(index))
self._iptv_progress_bar.set_fraction(index / size)
yield True
self._iptv_count_label.set_text(str(len(self._iptv_model)))
self._iptv_services_load_spinner.stop()
self._iptv_progress_bar.hide()
yield True
def get_new_background(self, flags):
@@ -2587,7 +2661,7 @@ class Application(Gtk.Application):
def save_data(self, callback=None, ext_path=None):
self._save_tool_button.set_sensitive(False)
profile = self._s_type
path = ext_path or self._settings.profile_data_path
path = ext_path or self._ext_data_path or self._settings.profile_data_path
backup_path = self._settings.profile_backup_path
# Backup data or clearing data path
backup_data(path, backup_path) if not ext_path and self._settings.backup_before_save else clear_data_path(path)
@@ -2615,6 +2689,8 @@ class Application(Gtk.Application):
if profile is SettingsType.ENIGMA_2:
# Blacklist.
write_blacklist(path, self._blacklist)
# Stream relay.
self._stream_relay.save(path)
self._save_tool_button.set_sensitive(True)
yield True
@@ -2716,7 +2792,8 @@ class Application(Gtk.Application):
if not cas:
return
cvs = list(filter(lambda val: val.startswith("C:") and len(val) > 3, cas.split(",")))
self._cas_label.set_text(", ".join(map(str, sorted(set(CAS.get(v[:4].upper(), def_val) for v in cvs)))))
cas = sorted(set(CAS.get(v.upper(), CAS.get(v[:4].upper(), def_val)) for v in cvs))
self._cas_label.set_text(", ".join(map(str, cas)))
def on_bouquets_selection(self, model, path, column):
self.reset_view_sort_indication(self._fav_view)
@@ -2767,13 +2844,14 @@ class Application(Gtk.Application):
ex_srv_name = ex_services.get(srv_id)
if srv:
background = self._EXTRA_COLOR if self._use_colors and ex_srv_name else None
coded = LINK_ICON if srv_id in self._stream_relay else srv.coded
srv_type = srv.service_type
is_marker = srv_type in self.MARKER_TYPES
if not is_marker:
num += 1
self._fav_model.append((0 if is_marker else num, srv.coded, ex_srv_name if ex_srv_name else srv.service,
self._fav_model.append((0 if is_marker else num, coded, ex_srv_name if ex_srv_name else srv.service,
srv.locked, srv.hide, srv_type, srv.pos, srv.fav_id,
None, None, background))
@@ -2945,21 +3023,33 @@ class Application(Gtk.Application):
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK
shift = event.state & Gdk.ModifierType.SHIFT_MASK
model_name, model = get_model_data(view)
if key is KeyboardKey.LEFT or key is KeyboardKey.RIGHT:
view.do_unselect_all(view)
elif ctrl and model_name == self.FAV_MODEL:
if key is KeyboardKey.P:
self.emit("fav-clicked", PlaybackMode.STREAM)
if key is KeyboardKey.W:
self.emit("fav-clicked", PlaybackMode.ZAP_PLAY)
if key is KeyboardKey.Z:
self.emit("fav-clicked", PlaybackMode.ZAP)
elif key is KeyboardKey.CTRL_L or key is KeyboardKey.CTRL_R:
if model_name == self.FAV_MODEL:
if ctrl and key in (KeyboardKey.CTRL_L, KeyboardKey.CTRL_R):
self.update_fav_num_column(model)
self.update_bouquet_list()
if shift:
if key is KeyboardKey.P:
self.emit("fav-clicked", PlaybackMode.STREAM)
if key is KeyboardKey.W:
self.emit("fav-clicked", PlaybackMode.ZAP_PLAY)
if key is KeyboardKey.Z:
self.emit("fav-clicked", PlaybackMode.ZAP)
elif model_name == self.SERVICE_MODEL:
if shift:
if key is KeyboardKey.P:
self.emit("srv-clicked", PlaybackMode.STREAM)
if key is KeyboardKey.W:
self.emit("srv-clicked", PlaybackMode.ZAP_PLAY)
if key is KeyboardKey.Z:
self.emit("srv-clicked", PlaybackMode.ZAP)
def on_view_focus(self, view, focus_event=None):
# Preventing focus lack for some cases.
if not focus_event and not view.is_focus():
@@ -2977,6 +3067,10 @@ class Application(Gtk.Application):
self._tool_elements[elem].set_sensitive(not_empty)
if elem == "bouquets_paste_popup_item":
self._tool_elements[elem].set_sensitive(not_empty and self._bouquets_buffer)
elif model_name == self.ALT_MODEL:
for elem in self._ALT_ELEMENTS:
if elem == "alt_paste_popup_item":
self._tool_elements[elem].set_sensitive(not is_service and self._rows_buffer)
else:
for elem in self._FAV_ELEMENTS:
if elem in ("paste_tool_button", "fav_paste_popup_item"):
@@ -2985,18 +3079,15 @@ class Application(Gtk.Application):
self._tool_elements[elem].set_sensitive(self._bq_selected and not is_service)
else:
self._tool_elements[elem].set_sensitive(not_empty and not is_service)
for elem in self._SERVICE_ELEMENTS:
self._tool_elements[elem].set_sensitive(not_empty and is_service)
for elem in self._BOUQUET_ELEMENTS:
self._tool_elements[elem].set_sensitive(False)
[self._tool_elements[elem].set_sensitive(not_empty and is_service) for elem in self._SERVICE_ELEMENTS]
[self._tool_elements[elem].set_sensitive(False) for elem in self._BOUQUET_ELEMENTS]
for elem in self._FAV_IPTV_ELEMENTS:
is_iptv = self._bq_selected and not is_service
if self._s_type is SettingsType.NEUTRINO_MP:
is_iptv = is_iptv and BqType(self._bq_selected.split(":")[1]) is BqType.WEBTV
self._tool_elements[elem].set_sensitive(is_iptv)
for elem in self._COMMONS_ELEMENTS:
self._tool_elements[elem].set_sensitive(not_empty)
[self._tool_elements[elem].set_sensitive(not_empty) for elem in self._COMMONS_ELEMENTS]
if self._s_type is not SettingsType.ENIGMA_2:
for elem in self._FAV_ENIGMA_ELEMENTS:
@@ -3313,7 +3404,11 @@ class Application(Gtk.Application):
if self._s_type is not SettingsType.ENIGMA_2:
self.show_error_message("Not allowed in this context!")
return
ServicesUpdateDialog(self).show()
if self._page is Page.SATELLITE:
self._satellite_tool.on_update()
else:
ServicesUpdateDialog(self).show()
@run_idle
def on_import_data_from_web(self, services, bouquets=None):
@@ -3410,6 +3505,9 @@ class Application(Gtk.Application):
# ***************** Extra tools ******************** #
def on_boot_logo_tool_show(self, action, value=None):
BootLogoManager(self).show()
def on_telnet_show(self, action, value=False):
action.set_state(value)
self._telnet_box.set_visible(value)
@@ -3763,7 +3861,7 @@ class Application(Gtk.Application):
def filter_set_default(self):
""" Setting defaults for filter elements. """
self._filter_entry.set_text("")
self._filter_only_free_button.set_active(False)
self._filter_all_button.set_active(True)
self._filter_not_in_bq_button.set_active(False)
self._filter_types_model.foreach(lambda m, p, i: m.set_value(i, 1, True))
self._service_types.update({r[0] for r in self._filter_types_model})
@@ -3814,7 +3912,7 @@ class Application(Gtk.Application):
@run_with_delay(1)
def on_filter_changed(self, item=None):
self._services_load_spinner.start()
self._services_progress_bar.show()
self.update_filter_cache()
self.update_filter_state()
@@ -3844,7 +3942,7 @@ class Application(Gtk.Application):
yield True
view.set_model(main_model)
GLib.idle_add(self._services_load_spinner.stop)
GLib.idle_add(self._services_progress_bar.hide)
def update_filter_cache(self):
self._filter_cache.clear()
@@ -3854,7 +3952,12 @@ class Application(Gtk.Application):
txt = self._filter_entry.get_text().upper()
for r in self._services_model:
fav_id = r[Column.SRV_FAV_ID]
free = not r[Column.SRV_CODED] if self._filter_only_free_button.get_active() else True
free = True
if self._filter_free_button.get_active():
free = not r[Column.SRV_CODED]
elif self._filter_coded_button.get_active():
free = r[Column.SRV_CODED]
self._filter_cache[fav_id] = all((free, fav_id not in self._in_bouquets,
r[Column.SRV_TYPE] in self._service_types,
r[Column.SRV_POS] in self._sat_positions,
@@ -3945,10 +4048,14 @@ class Application(Gtk.Application):
if model_name == self.IPTV_MODEL:
self.on_iptv_service_edit(model[paths][Column.IPTV_FAV_ID], view)
else:
ServiceDetailsDialog(self, self._NEW_COLOR).show()
ServiceDetailsDialog(self).show()
def on_services_add_new(self, item):
ServiceDetailsDialog(self, action=Action.ADD).show()
if self._s_type is SettingsType.NEUTRINO_MP:
self.show_error_message("Neutrino at the moment not supported!")
return
ServiceDetailsDialog(self, action=Action.ADD, tr_type=TrType(item.get_name())).show()
def on_bouquets_edit(self, view):
""" Renaming bouquets. """
@@ -4096,18 +4203,18 @@ class Application(Gtk.Application):
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def mark_not_in_bouquets(self):
self._services_load_spinner.start()
self._services_progress_bar.show()
ids = set(chain.from_iterable(self._bouquets.values()))
for index, row in enumerate(self._services_model):
fav_id = row[Column.SRV_FAV_ID]
if fav_id not in ids:
row[Column.SRV_BACKGROUND] = self._EXTRA_COLOR
bg = self.get_new_background(row[Column.SRV_CAS_FLAGS]) if fav_id in ids else self._EXTRA_COLOR
row[Column.SRV_BACKGROUND] = bg
if index % self.FAV_FACTOR == 0:
yield True
self._services_load_spinner.stop()
self._services_progress_bar.hide()
yield True
def on_services_clear_marked(self, item):
@@ -4119,13 +4226,13 @@ class Application(Gtk.Application):
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def clear_marked(self):
self._services_load_spinner.start()
self._services_progress_bar.show()
for index, row in enumerate(self._services_model):
row[Column.SRV_BACKGROUND] = self.get_new_background(row[Column.SRV_CAS_FLAGS])
if index % self.FAV_FACTOR == 0:
yield True
self._services_load_spinner.stop()
self._services_progress_bar.hide()
yield True
def on_services_clear_new_marked(self, item):
@@ -4142,7 +4249,7 @@ class Application(Gtk.Application):
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def clear_new_marked(self, model, paths):
self._services_load_spinner.start()
self._services_progress_bar.show()
paths = get_base_paths(paths, model)
model = get_base_model(model)
@@ -4173,7 +4280,7 @@ class Application(Gtk.Application):
yield True
self.show_info_message("Done!", Gtk.MessageType.INFO)
self._services_load_spinner.stop()
self._services_progress_bar.hide()
yield True
# ***************** Picons ********************* #
@@ -4324,6 +4431,9 @@ class Application(Gtk.Application):
model.set(model.get_iter(paths), data)
self._fav_view.row_activated(paths[0], self._fav_view.get_column(Column.FAV_NUM))
def on_alt_paste(self, item):
self.on_paste(self._alt_view, ViewTarget.ALT)
def delete_alts(self, itrs, model, rows):
""" Deleting alternatives. """
list(map(model.remove, itrs))
@@ -4365,6 +4475,8 @@ class Application(Gtk.Application):
itr_str, sep, source = txt.partition(self.DRAG_SEP)
if source == self.SERVICE_MODEL:
model, id_col, t_col = self._services_view.get_model(), Column.SRV_FAV_ID, Column.SRV_TYPE
elif source == self.IPTV_MODEL:
model, id_col, t_col = self._iptv_services_view.get_model(), Column.IPTV_FAV_ID, Column.IPTV_TYPE
elif source == self.FAV_MODEL:
model, id_col, t_col = self._fav_view.get_model(), Column.FAV_ID, Column.FAV_TYPE
elif source == self.ALT_MODEL:
@@ -4378,20 +4490,8 @@ class Application(Gtk.Application):
itrs = tuple(model.get_iter_from_string(itr) for itr in itr_str.split(","))
types = {BqServiceType.MARKER.name, BqServiceType.SPACE.name, BqServiceType.ALT.name}
ids = tuple(model.get_value(itr, id_col) for itr in itrs if model.get_value(itr, t_col) not in types)
srvs = tuple(self._services.get(f_id, None) for f_id in ids)
dt, it = BqServiceType.DEFAULT, BqServiceType.IPTV
a_srvs = tuple(BouquetService(None, dt if s.service_type != it.name else it, s.fav_id, 0) for s in srvs)
alt_services = srv.transponder + a_srvs
self._services[srv.fav_id] = srv._replace(transponder=alt_services)
a_row = self._alt_model[self._alt_model.get_iter_first()][:]
alt_id, a_itr = a_row[Column.ALT_ID], a_row[Column.ALT_ITER]
for i, srv in enumerate(srvs, start=len(self._alt_model) + 1):
pic = self._picons.get(srv.picon_id, None)
self._alt_model.append((i, pic, srv.service, srv.service_type, srv.pos, srv.fav_id, alt_id, a_itr))
return True
return self.append_alt_services(srv, tuple(self._services.get(f_id, None) for f_id in ids))
def on_alt_move(self, s_iters, info, srv):
""" Move alternatives in the list. """
@@ -4427,6 +4527,86 @@ class Application(Gtk.Application):
if srv and srv.transponder or row[Column.ALT_TYPE] == BqServiceType.IPTV.name:
self.emit("fav-changed", srv)
def on_alt_view_key_press(self, view, event):
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK
if ctrl and key == KeyboardKey.V:
self.alt_paste()
elif key == KeyboardKey.DELETE:
self.on_delete(view)
def alt_paste(self):
srv = self._services.get(self._alt_model.get_value(self._alt_model.get_iter_first(), Column.ALT_ID), None)
if not srv:
return
self.append_alt_services(srv, tuple(self._services.get(r[Column.FAV_ID]) for r in self._rows_buffer))
self._rows_buffer.clear()
def append_alt_services(self, srv, srvs):
dt, it = BqServiceType.DEFAULT, BqServiceType.IPTV
a_srvs = tuple(BouquetService(None, dt if s.service_type != it.name else it, s.fav_id, 0) for s in srvs)
alt_services = srv.transponder + a_srvs
self._services[srv.fav_id] = srv._replace(transponder=alt_services)
a_row = self._alt_model[self._alt_model.get_iter_first()][:]
alt_id, a_itr = a_row[Column.ALT_ID], a_row[Column.ALT_ITER]
for i, srv in enumerate(srvs, start=len(self._alt_model) + 1):
pic = self._picons.get(srv.picon_id, None)
self._alt_model.append((i, pic, srv.service, srv.service_type, srv.pos, srv.fav_id, alt_id, a_itr))
return True
# ***************** Stream relay ********************** #
def on_use_streamrelay(self, item):
gen = self.update_streamrelay_use(remove=False)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def on_remove_use_streamrelay(self, item):
gen = self.update_streamrelay_use(remove=True)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def update_streamrelay_use(self, remove):
model, paths = self._fav_view.get_selection().get_selected_rows()
if not paths:
return
skip_types = {BqServiceType.MARKER, BqServiceType.SPACE}
count = 0
for p in paths:
s_type = BqServiceType(model[p][Column.FAV_TYPE])
if s_type in skip_types:
continue
srv = self._services.get(model[p][Column.FAV_ID], None)
if not srv:
continue
srv_id = srv.data_id if s_type is BqServiceType.IPTV else srv.fav_id
if remove:
if self._stream_relay.pop(srv_id, None):
model[p][Column.FAV_CODED] = srv.coded
count += 1
else:
model[p][Column.FAV_CODED] = LINK_ICON
if s_type is BqServiceType.IPTV:
ref = f"{srv_id.replace('%3A', '%3a')}:"
else:
ref = f"{self.get_service_ref_data(srv)}:"
self._stream_relay[srv_id] = ref
count += 1
yield True
self.show_info_message(f"{translate('Count of successfully configured services:')} {count}")
# ***************** Profile label ********************* #
@run_idle
@@ -4457,8 +4637,8 @@ class Application(Gtk.Application):
self._info_bar.set_visible(False)
def is_data_loading(self):
is_services_loading = self._services_load_spinner.get_property("active")
return is_services_loading or self._iptv_services_load_spinner.get_property("active")
is_services_loading = self._services_progress_bar.get_visible()
return is_services_loading or self._iptv_progress_bar.get_visible()
def is_data_saved(self):
if self._data_hash != 0 and self._data_hash != self.get_data_hash():
@@ -4471,6 +4651,7 @@ class Application(Gtk.Application):
""" Returns the sum of all data hash. """
return sum(map(hash, map(frozenset, (self._services.items(),
self._bouquets.keys(),
self._stream_relay.keys(),
map(tuple, self._bouquets.values())))))
# ******************* Properties ***********************#

View File

@@ -49,7 +49,7 @@ from gi.repository import GdkPixbuf, GLib, Gio
from app.eparser import Service
from app.eparser.ecommons import Flag, BouquetService, Bouquet, BqType
from app.eparser.enigma.bouquets import BqServiceType, to_bouquet_id
from app.eparser.enigma.bouquets import BqServiceType
from app.settings import SettingsType, SEP, IS_WIN, IS_DARWIN, IS_LINUX
from .dialogs import show_dialog, DialogType, translate
from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column
@@ -294,7 +294,7 @@ def set_lock(blacklist, services, model, paths, target, services_model):
fav_id = model.get_value(itr, Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID)
srv = services.get(fav_id, None)
if srv and srv.service_type not in skip_type:
bq_id = srv.data_id if srv.service_type == BqServiceType.IPTV.name else to_bouquet_id(srv)
bq_id = srv.data_id if srv.service_type == BqServiceType.IPTV.name else srv.fav_id
if not bq_id:
continue
blacklist.discard(bq_id) if locked else blacklist.add(bq_id)
@@ -819,7 +819,10 @@ def get_pos_num(pos):
if len(pos) > 1:
m = -1 if pos[-1] == "W" else 1
return float(pos[:-1]) * m
try:
return float(pos[:-1]) * m
except ValueError:
return -183
return -181.0 if pos == "T" else -182.0
@@ -855,7 +858,7 @@ def get_iptv_data(fav_id):
def on_popup_menu(menu, event):
""" Shows popup menu for the view """
""" Shows popup menu for the view. """
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
menu.popup(None, None, None, None, event.button, event.time)
@@ -868,5 +871,14 @@ def show_info_bar_message(bar, label, text, message_type=Gtk.MessageType.INFO):
bar.set_visible(True)
def redraw_image(area, cr, pixbuf):
""" Helper method to redraw (auto resize) image in the Gtk DrawingArea. """
cr.scale(area.get_allocated_width() / pixbuf.get_width(),
area.get_allocated_height() / pixbuf.get_height())
img_surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, 1, None)
cr.set_source_surface(img_surface, 0, 0)
cr.paint()
if __name__ == "__main__":
pass

View File

@@ -347,7 +347,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkCheckButton" id="info_check_button">
<property name="visible">True</property>
<property name="visible" bind-source="filter_button" bind-property="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">False</property>
<property name="tooltip-text" translatable="yes">Details</property>
@@ -413,12 +413,13 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="header_download_box">
<property name="visible">True</property>
<property name="visible" bind-source="cancel_button" bind-property="visible" bind-flags="invert-boolean">True</property>
<property name="visible" bind-source="convert_button" bind-property="visible" bind-flags="invert-boolean">True</property>
<property name="can-focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkMenuButton" id="add_menu_button">
<property name="visible">True</property>
<property name="visible" bind-source="manager_button" bind-property="active">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
@@ -440,6 +441,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkButton" id="receive_button">
<property name="visible" bind-source="download_source_button" bind-property="visible">False</property>
<property name="sensitive">False</property>
<property name="can-focus">False</property>
<property name="receives-default">False</property>
@@ -462,7 +464,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkButton" id="remove_button">
<property name="visible">True</property>
<property name="visible" bind-source="filter_button" bind-property="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Remove all picons from the receiver</property>
@@ -490,7 +492,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkToggleButton" id="src_button">
<property name="visible">True</property>
<property name="visible" bind-source="filter_button" bind-property="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Additional source</property>
@@ -529,6 +531,7 @@ Author: Dmitriy Yefremov
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="filter_bar">
<property name="visible" bind-source="filter_button" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="spacing">5</property>
@@ -591,6 +594,7 @@ Author: Dmitriy Yefremov
<property name="wide-handle">True</property>
<child>
<object class="GtkFrame" id="src_picon_box_frame">
<property name="visible" bind-source="src_button" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="label-xalign">0.49000000953674316</property>
<property name="shadow-type">none</property>
@@ -607,6 +611,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkCheckButton" id="src_filter_button">
<property name="label" translatable="yes">Filter</property>
<property name="visible" bind-source="filter_button" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">False</property>
@@ -741,6 +746,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkCheckButton" id="dst_filter_button">
<property name="label" translatable="yes">Filter</property>
<property name="visible" bind-source="filter_button" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">False</property>
@@ -849,6 +855,7 @@ Author: Dmitriy Yefremov
</child>
<child type="label">
<object class="GtkLabel" id="explorer_dst_label">
<property name="visible" bind-source="src_button" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Destination:</property>
</object>
@@ -868,6 +875,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkFrame" id="explorer_info_box_frame">
<property name="visible" bind-source="info_check_button" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
@@ -952,7 +960,7 @@ Author: Dmitriy Yefremov
<property name="margin-bottom">5</property>
<child>
<object class="GtkComboBoxText" id="download_source_button">
<property name="sensitive">False</property>
<property name="sensitive" bind-source="satellite_label" bind-property="visible">False</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Source:</property>
<property name="active">0</property>
@@ -1026,7 +1034,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkLabel" id="loading_data_label">
<property name="visible">True</property>
<property name="visible" bind-source="satellite_label" bind-property="visible" bind-flags="invert-boolean">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Loading data...</property>
<property name="ellipsize">end</property>
@@ -1039,10 +1047,10 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkSpinner" id="loading_data_spinner">
<property name="visible">True</property>
<property name="visible" bind-source="satellite_label" bind-property="visible" bind-flags="invert-boolean">True</property>
<property name="can-focus">False</property>
<property name="margin-right">5</property>
<property name="active">True</property>
<property name="active" bind-source="satellite_label" bind-property="visible" bind-flags="invert-boolean">True</property>
</object>
<packing>
<property name="expand">False</property>
@@ -1055,6 +1063,7 @@ Author: Dmitriy Yefremov
<object class="GtkGrid" id="satellite_filter_grid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Filter by current satellite positions</property>
<property name="margin-right">5</property>
<property name="column-spacing">5</property>
<child>
@@ -1072,7 +1081,6 @@ Author: Dmitriy Yefremov
<object class="GtkSwitch" id="satellite_filter_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="active">True</property>
<signal name="state-set" handler="on_satellite_filter_toggled" swapped="no"/>
</object>
<packing>
@@ -1103,6 +1111,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkTreeView" id="satellites_view">
<property name="visible">True</property>
<property name="sensitive" bind-source="satellite_label" bind-property="visible">False</property>
<property name="can-focus">True</property>
<property name="model">satellites_list_store</property>
<property name="headers-visible">False</property>
@@ -1571,110 +1580,24 @@ Author: Dmitriy Yefremov
<object class="GtkBox" id="converter_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-left">5</property>
<property name="margin-right">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<property name="spacing">5</property>
<child>
<!-- n-columns=3 n-rows=4 -->
<object class="GtkGrid" id="converter_grid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-left">10</property>
<property name="margin-right">10</property>
<property name="margin-top">10</property>
<property name="row-spacing">5</property>
<property name="column-spacing">2</property>
<property name="column-homogeneous">True</property>
<child>
<object class="GtkFileChooserButton" id="enigma2_path_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="action">select-folder</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="picons_path_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Path to Enigma2 picons:</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="save_to_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Path to save:</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkFileChooserButton" id="save_to_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="action">select-folder</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">3</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="convert_to_label">
<property name="visible">True</property>
<object class="GtkLabel" id="convert_to_nt_label">
<property name="visible" bind-source="converter_nt_button" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="margin-bottom">10</property>
<property name="label" translatable="yes">Enigma2 -&gt; Neutrino-MP</property>
<property name="label">Enigma2 -&gt; Neutrino-MP</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
<property name="position">0</property>
</packing>
</child>
<child>
@@ -1688,9 +1611,154 @@ Author: Dmitriy Yefremov
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="convert_to_sc_label">
<property name="visible" bind-source="converter_sc_button" bind-property="active">True</property>
<property name="can-focus">False</property>
<property name="margin-bottom">10</property>
<property name="label">Enigma2 -&gt; OSCam</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="converter_format_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="margin-top">10</property>
<property name="homogeneous">True</property>
<property name="layout-style">expand</property>
<child>
<object class="GtkRadioButton" id="converter_sc_button">
<property name="label" translatable="yes">OSCam</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">False</property>
<property name="group">converter_nt_button</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="converter_nt_button">
<property name="label" translatable="yes">Neutrino-MP</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">False</property>
<property name="group">converter_sc_button</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="converter_select_bq_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="coverter_bq_label">
<property name="label" translatable="yes">Convert for selected bouquets</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="converter_bq_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="picons_path_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Path to Enigma2 picons:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkFileChooserButton" id="enigma2_path_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="action">select-folder</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="save_to_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Path to save:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkFileChooserButton" id="save_to_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="action">select-folder</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">7</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
@@ -1807,7 +1875,7 @@ Author: Dmitriy Yefremov
<property name="margin-bottom">2</property>
<child>
<object class="GtkLabel" id="manager_label">
<property name="visible">True</property>
<property name="visible" bind-source="manager_button" bind-property="active">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Picons manager</property>
<attributes>
@@ -1822,6 +1890,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkLabel" id="downloader_label">
<property name="visible" bind-source="downloader_button" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Picons download tool</property>
<attributes>
@@ -1836,8 +1905,9 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkLabel" id="converter_label">
<property name="visible" bind-source="converter_button" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Converter between name formats</property>
<property name="label" translatable="yes">Converter between formats</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>

View File

@@ -40,11 +40,11 @@ from app.commons import run_idle, run_task, run_with_delay, log
from app.connections import upload_data, DownloadType, download_data, remove_picons
from app.settings import SettingsType, Settings, SEP, IS_DARWIN
from app.tools.picons import (PiconsParser, parse_providers, Provider, convert_to, download_picon, PiconsCzDownloader,
PiconsError)
PiconsError, PiconFormat)
from app.tools.satellites import SatellitesParser, SatelliteSource
from .dialogs import show_dialog, DialogType, translate, get_builder, get_chooser_dialog
from .main_helper import (scroll_to, on_popup_menu, get_base_model, set_picon, get_picon_pixbuf, get_picon_dialog,
get_picon_file_name, get_pixbuf_from_data, get_pixbuf_at_scale)
get_picon_file_name, get_pixbuf_from_data, get_pixbuf_at_scale, get_pos_num)
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey, Page, ViewTarget
@@ -153,13 +153,9 @@ class PiconManager(Gtk.Box):
self._bouquet_filter_switch = builder.get_object("bouquet_filter_switch")
self._providers_header_box = builder.get_object("providers_header_box")
self._header_download_box = builder.get_object("header_download_box")
self._satellite_label.bind_property("visible", builder.get_object("loading_data_label"), "visible", 4)
self._satellite_label.bind_property("visible", builder.get_object("loading_data_spinner"), "visible", 4)
self._satellite_label.bind_property("visible", self._download_source_button, "sensitive")
self._satellite_label.bind_property("visible", self._satellites_view, "sensitive")
self._cancel_button.bind_property("visible", self._header_download_box, "visible", 4)
self._convert_button.bind_property("visible", self._header_download_box, "visible", 4)
self._download_source_button.bind_property("visible", self._receive_button, "visible")
self._converter_sc_button = builder.get_object("converter_sc_button")
self._converter_nt_button = builder.get_object("converter_nt_button")
self._converter_bq_button = builder.get_object("converter_bq_button")
# Info.
self._dst_count_label = builder.get_object("dst_count_label")
self._info_check_button = builder.get_object("info_check_button")
@@ -169,24 +165,11 @@ class PiconManager(Gtk.Box):
self._filter_bar = builder.get_object("filter_bar")
self._auto_filter_switch = builder.get_object("auto_filter_switch")
self._filter_button = builder.get_object("filter_button")
self._filter_button.bind_property("active", self._filter_bar, "visible")
self._filter_button.bind_property("active", self._src_filter_button, "visible")
self._filter_button.bind_property("active", self._dst_filter_button, "visible")
self._filter_button.bind_property("visible", self._info_check_button, "visible")
self._filter_button.bind_property("visible", self._remove_button, "visible")
self._src_button = builder.get_object("src_button")
self._src_button.bind_property("active", builder.get_object("explorer_dst_label"), "visible")
self._src_button.bind_property("active", builder.get_object("src_picon_box_frame"), "visible")
self._filter_button.bind_property("visible", self._src_button, "visible")
self._info_check_button.bind_property("active", builder.get_object("explorer_info_box_frame"), "visible")
# Header buttons. -> Used instead stack switcher.
self._manager_button = builder.get_object("manager_button")
self._manager_button.bind_property("active", builder.get_object("manager_label"), "visible")
self._downloader_button = builder.get_object("downloader_button")
self._downloader_button.bind_property("active", builder.get_object("downloader_label"), "visible")
self._converter_button = builder.get_object("converter_button")
self._converter_button.bind_property("active", builder.get_object("converter_label"), "visible")
self._manager_button.bind_property("active", builder.get_object("add_menu_button"), "visible")
# Init drag-and-drop
self.init_drag_and_drop()
# Rendering.
@@ -223,6 +206,8 @@ class PiconManager(Gtk.Box):
name = "downloader"
elif is_converter:
name = "converter"
if not self._enigma2_path_button.get_filename():
self._enigma2_path_button.set_filename(self._settings.profile_picons_path)
self._stack.set_visible_child_name(name)
@@ -252,6 +237,7 @@ class PiconManager(Gtk.Box):
def on_profile_changed(self, app, data):
self._current_path_label.set_text(self._settings.profile_picons_path)
self.update_picons_dest(app, self._app.page)
self._enigma2_path_button.set_filename(self._settings.profile_picons_path)
def on_picon_assign(self, app, target):
if target is ViewTarget.SERVICES:
@@ -680,7 +666,7 @@ class PiconManager(Gtk.Box):
model.clear()
try:
for sat in sorted(sats):
for sat in sorted(sats, key=lambda s: get_pos_num(s[1]), reverse=True):
pos = sat[1]
name = f"{sat[0]} ({pos})"
if is_filter and pos not in self._sat_positions:
@@ -825,9 +811,10 @@ class PiconManager(Gtk.Box):
services = self._app.current_services
ids = set()
for s in (services.get(fav_id) for fav_id in fav_bouquet):
ids.add(s.picon_id)
ids.add(get_picon_file_name(s.service))
for s in (services.get(fav_id, None) for fav_id in fav_bouquet):
if s:
ids.add(s.picon_id)
ids.add(get_picon_file_name(s.service))
return ids
def process_provider(self, prv, picons_path):
@@ -1018,9 +1005,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

View File

@@ -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:
@@ -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. """

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2024 Dmitriy Yefremov
# Copyright (c) 2018-2025 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -143,7 +143,8 @@ class RecordingsTool(Gtk.Box):
if self._ftp:
self._ftp.close()
self._ftp = UtfFTP(host=self._settings.host, user=self._settings.user, passwd=self._settings.password)
host, port = self._settings.host, self._settings.port
self._ftp = UtfFTP(host=host, port=port, user=self._settings.user, passwd=self._settings.password)
self._ftp.encoding = "utf-8"
except all_errors:
pass # NOP

View File

@@ -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 satellite list editor for GNU/Linux. -->
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2024 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">

View File

@@ -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
@@ -38,7 +38,7 @@ 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"
@@ -47,7 +47,7 @@ _UI_PATH = UI_RESOURCES_PATH + "service_details_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,7 +88,7 @@ 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
@@ -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[7])
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]))
@@ -401,11 +439,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 +456,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 +489,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 +517,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 +640,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 +662,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,8 +671,8 @@ 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()
@@ -645,9 +692,10 @@ class ServiceDetailsDialog:
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}:{flag}:{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 +706,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 +714,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 +729,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 +762,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 +842,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()

View File

@@ -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>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2024 Dmitriy Yefremov
# Copyright (c) 2018-2025 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -185,6 +185,7 @@ class SettingsDialog:
self._enable_yt_dl_switch = builder.get_object("enable_yt_dl_switch")
self._enable_update_yt_dl_switch = builder.get_object("enable_update_yt_dl_switch")
self._enable_send_to_switch = builder.get_object("enable_send_to_switch")
self._enable_epg_name_cache_switch = builder.get_object("enable_epg_name_cache_switch")
self._enable_exp_switch = builder.get_object("enable_experimental_switch")
# Profiles.
self._profile_view = builder.get_object("profile_tree_view")
@@ -268,7 +269,13 @@ class SettingsDialog:
def on_response(self, dialog, resp):
if resp == Gtk.ResponseType.ACCEPT:
self._updated = self.on_save_settings()
dialog.destroy()
if not self._updated:
return True
if resp == Gtk.ResponseType.DELETE_EVENT or resp == Gtk.ResponseType.ACCEPT:
dialog.destroy()
return False
def on_field_button_press(self, entry):
update_entry_data(entry, self._dialog, self._settings)
@@ -290,12 +297,12 @@ class SettingsDialog:
self._hosts_box.remove_all()
self._remove_host_button.set_sensitive(len([self._hosts_box.append(h, h) for h in self._settings.hosts]) > 1)
self._hosts_box.set_active_id(self._settings.host)
self._port_field.set_text(self._settings.port)
self._port_field.set_text(str(self._settings.port))
self._login_field.set_text(self._settings.user)
self._password_field.set_text(self._settings.password)
self._http_port_field.set_text(self._settings.http_port)
self._http_port_field.set_text(str(self._settings.http_port))
self._http_use_ssl_check_button.set_active(self._settings.http_use_ssl)
self._telnet_port_field.set_text(self._settings.telnet_port)
self._telnet_port_field.set_text(str(self._settings.telnet_port))
self._telnet_timeout_spin_button.set_value(self._settings.telnet_timeout)
self._services_field.set_text(self._settings.services_path)
self._user_bouquet_field.set_text(self._settings.user_bouquet_path)
@@ -338,6 +345,7 @@ class SettingsDialog:
self._enable_yt_dl_switch.set_active(self._settings.enable_yt_dl)
self._enable_update_yt_dl_switch.set_active(self._settings.enable_yt_dl_update)
self._enable_send_to_switch.set_active(self._settings.enable_send_to)
self._enable_epg_name_cache_switch.set_active(self._settings.enable_epg_name_cache)
self._set_color_switch.set_active(self._settings.use_colors)
new_rgb = Gdk.RGBA()
new_rgb.parse(self._settings.new_color)
@@ -351,29 +359,34 @@ class SettingsDialog:
def on_apply_profile_settings(self, item=None):
if not self.is_data_correct(self._digit_elems):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
return False
self._s_type = SettingsType(int(self._settings_type_box.get_active_id()))
self._settings.setting_type = self._s_type
self._settings.host = self._host_field.get_text()
self._settings.hosts = [h[1] for h in self._hosts_box.get_model()]
self._settings.port = self._port_field.get_text()
self._settings.port = int(self._port_field.get_text())
self._settings.user = self._login_field.get_text()
self._settings.password = self._password_field.get_text()
self._settings.http_port = self._http_port_field.get_text()
self._settings.http_port = int(self._http_port_field.get_text())
self._settings.http_use_ssl = self._http_use_ssl_check_button.get_active()
self._settings.telnet_port = self._telnet_port_field.get_text()
self._settings.telnet_port = int(self._telnet_port_field.get_text())
self._settings.telnet_timeout = int(self._telnet_timeout_spin_button.get_value())
self._settings.services_path = self._services_field.get_text()
self._settings.satellites_xml_path = self._satellites_xml_field.get_text()
self._settings.user_bouquet_path = self._user_bouquet_field.get_text()
self._settings.epg_dat_path = self._epg_dat_box.get_active_id()
self._settings.picons_path = self._picons_paths_box.get_active_id()
return True
def on_save_settings(self, item=None):
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
return False
self.on_apply_profile_settings()
if not self.on_apply_profile_settings():
return False
self._ext_settings.profiles = self._settings.profiles
self._ext_settings.backup_before_save = self._before_save_switch.get_active()
self._ext_settings.backup_before_downloading = self._before_downloading_switch.get_active()
@@ -420,6 +433,7 @@ class SettingsDialog:
self._ext_settings.enable_yt_dl = self._enable_yt_dl_switch.get_active()
self._ext_settings.enable_yt_dl_update = self._enable_update_yt_dl_switch.get_active()
self._ext_settings.enable_send_to = self._enable_send_to_switch.get_active()
self._ext_settings.enable_epg_name_cache = self._enable_epg_name_cache_switch.get_active()
self._ext_settings.default_profile = list(filter(lambda r: r[1], self._profile_view.get_model()))[0][0]
self._ext_settings.save()
@@ -430,6 +444,11 @@ class SettingsDialog:
def on_connection_test(self, item):
if self._test_spinner.get_state() is Gtk.StateType.ACTIVE:
return
if not self.is_data_correct((self._port_field, self._http_port_field, self._telnet_port_field)):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
self.show_spinner(True)
if self._ftp_radio_button.get_active():
self.test_ftp()
@@ -440,7 +459,7 @@ class SettingsDialog:
def test_http(self):
user, password = self._login_field.get_text(), self._password_field.get_text()
host, port = self._host_field.get_text(), self._http_port_field.get_text()
host, port = self._host_field.get_text(), int(self._http_port_field.get_text())
use_ssl = self._http_use_ssl_check_button.get_active()
try:
self.show_info_message(test_http(host, port, user, password, use_ssl=use_ssl, s_type=self._s_type),
@@ -454,7 +473,7 @@ class SettingsDialog:
def test_telnet(self):
timeout = int(self._telnet_timeout_spin_button.get_value())
host, port = self._host_field.get_text(), self._telnet_port_field.get_text()
host, port = self._host_field.get_text(), int(self._telnet_port_field.get_text())
user, password = self._login_field.get_text(), self._password_field.get_text()
try:
self.show_info_message(test_telnet(host, port, user, password, timeout), Gtk.MessageType.INFO)
@@ -464,7 +483,7 @@ class SettingsDialog:
self.show_spinner(False)
def test_ftp(self):
host, port = self._host_field.get_text(), self._port_field.get_text()
host, port = self._host_field.get_text(), int(self._port_field.get_text())
user, password = self._login_field.get_text(), self._password_field.get_text()
try:
self.show_info_message(f"OK. {test_ftp(host, port, user, password)}", Gtk.MessageType.INFO)
@@ -497,6 +516,7 @@ class SettingsDialog:
self._support_ver5_switch.set_active(state)
self._unlimited_buffer_switch.set_active(state)
self._enable_send_to_switch.set_active(state)
self._enable_epg_name_cache_switch.set_active(state)
self._enable_yt_dl_switch.set_active(state)
def on_force_bq_name(self, switch, state):

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 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,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
@@ -158,7 +131,7 @@ class TelnetClient(Gtk.Box):
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
return None
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK
@@ -181,6 +154,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()

View File

@@ -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>

View File

@@ -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
@@ -31,9 +31,8 @@ from datetime import datetime, timedelta
from enum import Enum
from urllib.parse import quote
from app.settings import USE_HEADER_BAR
from app.ui.main_helper import on_popup_menu
from .dialogs import get_builder, translate, show_dialog, DialogType
from .dialogs import get_builder, translate, show_dialog, DialogType, BaseDialog
from .uicommons import Gtk, Gdk, GLib, UI_RESOURCES_PATH, Page, Column, KeyboardKey, MOD_MASK
from ..commons import run_idle, log
from ..connections import HttpAPI
@@ -55,9 +54,11 @@ class TimerTool(Gtk.Box):
EVENT = 1
CHANGE = 2
class TimerDialog(Gtk.Dialog):
class TimerDialog(BaseDialog):
def __init__(self, parent, action=None, timer_data=None, *args, **kwargs):
super().__init__(use_header_bar=USE_HEADER_BAR, *args, **kwargs)
super().__init__(parent=parent, title="Timer",
buttons=(translate("Cancel"), Gtk.ResponseType.CANCEL,
translate("Save"), Gtk.ResponseType.OK), *args, **kwargs)
self._action = action or TimerTool.TimerAction.ADD
self._timer_data = timer_data or {}
@@ -71,14 +72,6 @@ class TimerTool(Gtk.Box):
"min_end_adjustment", "timer_begins_popover", "begins_hour_adjustment",
"min_begins_adjustment"))
self.set_title(translate("Timer"))
self.set_modal(True)
self.set_skip_pager_hint(True)
self.set_skip_taskbar_hint(True)
self.set_transient_for(parent)
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
self.set_resizable(False)
self._timer_name_entry = builder.get_object("timer_name_entry")
self._timer_desc_entry = builder.get_object("timer_desc_entry")
self._timer_service_entry = builder.get_object("timer_service_entry")
@@ -111,8 +104,7 @@ class TimerTool(Gtk.Box):
self._timer_desc_entry.drag_dest_unset()
self._timer_service_entry.drag_dest_unset()
self.add_buttons(translate("Cancel"), Gtk.ResponseType.CANCEL, translate("Save"), Gtk.ResponseType.OK)
self.get_content_area().pack_start(builder.get_object("timer_dialog_frame"), True, True, 5)
self.get_content_area().pack_start(builder.get_object("timer_dialog_frame"), True, True, 0)
if self._action is TimerTool.TimerAction.ADD:
self.set_timer_for_add()
@@ -363,9 +355,11 @@ class TimerTool(Gtk.Box):
if p_count == 1:
service = self._app.current_services.get(model[paths][Column.FAV_ID], None)
if service:
if service and service.picon_id:
self.add_timer({"e2servicename": service.service,
"e2servicereference": service.picon_id.rstrip(".png").replace("_", ":")})
else:
self._app.show_error_message("Not allowed in this context!")
elif p_count > 1:
self._app.show_error_message("Please, select only one item!")
else:

View File

@@ -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
@@ -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):

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2
<!-- Generated with glade 3.40.0
The MIT License (MIT)
Copyright (c) 2018-2023 Dmitriy Yefremov
Copyright (c) 2018-2025 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2025 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<!-- n-columns=2 n-rows=4 -->
<object class="GtkGrid" id="cable_tr_box">
@@ -471,7 +471,7 @@ Author: Dmitriy Yefremov
<object class="GtkSpinButton" id="sat_position_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="width-chars">5</property>
<property name="width-chars">6</property>
<property name="secondary-icon-activatable">False</property>
<property name="secondary-icon-sensitive">False</property>
<property name="input-purpose">number</property>
@@ -879,7 +879,7 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<!-- n-columns=2 n-rows=4 -->
<!-- n-columns=2 n-rows=5 -->
<object class="GtkGrid" id="tr_dialog_grid2">
<property name="visible">True</property>
<property name="can-focus">False</property>
@@ -911,7 +911,7 @@ Author: Dmitriy Yefremov
<property name="max-width-chars">12</property>
<property name="primary-icon-name">document-edit-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="placeholder-text" translatable="yes">0 - 262142</property>
<property name="placeholder-text">0 - 262142</property>
<property name="input-purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>
@@ -928,7 +928,7 @@ Author: Dmitriy Yefremov
<property name="max-width-chars">12</property>
<property name="primary-icon-name">document-edit-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="placeholder-text" translatable="yes">0 - 255</property>
<property name="placeholder-text">0 - 255</property>
<property name="input-purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>
@@ -945,7 +945,7 @@ Author: Dmitriy Yefremov
<property name="max-width-chars">12</property>
<property name="primary-icon-name">document-edit-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="placeholder-text" translatable="yes">0 - 255</property>
<property name="placeholder-text">0 - 255</property>
<property name="input-purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>
@@ -1095,6 +1095,58 @@ Author: Dmitriy Yefremov
<property name="top-attach">3</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkLabel" id="tr_t2mi_pid_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">T2-MI PID</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">4</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="t2mi_pid_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="width-chars">5</property>
<property name="max-width-chars">12</property>
<property name="primary-icon-name">document-edit-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="placeholder-text">0 - 8191</property>
<property name="input-purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2024 Dmitriy Yefremov
# Copyright (c) 2018-2025 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -207,6 +207,7 @@ class SatTransponderDialog(TransponderDialog):
self._pls_code_entry = builder.get_object("pls_code_entry")
self._is_id_entry = builder.get_object("is_id_entry")
self._t2mi_plp_id_entry = builder.get_object("t2mi_plp_id_entry")
self._t2mi_pid_entry = builder.get_object("t2mi_pid_entry")
self.set_style_provider(self._freq_entry)
self.set_style_provider(self._rate_entry)
@@ -230,6 +231,7 @@ class SatTransponderDialog(TransponderDialog):
self._is_id_entry.set_text(transponder.is_id if transponder.is_id else "")
self._pls_code_entry.set_text(transponder.pls_code if transponder.pls_code else "")
self._t2mi_plp_id_entry.set_text(transponder.t2mi_plp_id if transponder.t2mi_plp_id else "")
self._t2mi_pid_entry.set_text(transponder.t2mi_pid if transponder.t2mi_pid else "")
def to_transponder(self):
return Transponder(frequency=self._freq_entry.get_text(),
@@ -241,7 +243,8 @@ class SatTransponderDialog(TransponderDialog):
pls_mode=get_key_by_value(PLS_MODE, self._pls_mode_box.get_active_id()),
pls_code=self._pls_code_entry.get_text(),
is_id=self._is_id_entry.get_text(),
t2mi_plp_id=self._t2mi_plp_id_entry.get_text())
t2mi_plp_id=self._t2mi_plp_id_entry.get_text(),
t2mi_pid=self._t2mi_pid_entry.get_text())
def is_accept(self):
tr = self.to_transponder()
@@ -255,6 +258,8 @@ class SatTransponderDialog(TransponderDialog):
return False
elif self.digit_pattern.search(tr.t2mi_plp_id):
return False
elif self.digit_pattern.search(tr.t2mi_pid):
return False
return True
@@ -401,8 +406,6 @@ class UpdateDialog:
"on_satellite_changed": self.on_satellite_changed,
"on_transponder_toggled": self.on_transponder_toggled,
"on_info_bar_close": self.on_info_bar_close,
"on_filter_toggled": self.on_filter_toggled,
"on_find_toggled": self.on_find_toggled,
"on_popup_menu": on_popup_menu,
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
@@ -441,7 +444,7 @@ class UpdateDialog:
self._left_action_box = builder.get_object("sat_update_left_action_box")
self._right_action_box = builder.get_object("sat_update_right_action_box")
# Filter
self._filter_bar = builder.get_object("sat_update_filter_bar")
self._filter_bar_box = builder.get_object("filter_bar_box")
self._from_pos_button = builder.get_object("from_pos_button")
self._to_pos_button = builder.get_object("to_pos_button")
self._filter_from_combo_box = builder.get_object("filter_from_combo_box")
@@ -449,18 +452,16 @@ class UpdateDialog:
self._filter_model = builder.get_object("update_sat_list_model_filter")
self._filter_model.set_visible_func(self.filter_function)
self._filter_positions = (0, 0)
self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible")
# Log.
self._log_frame = builder.get_object("log_frame")
builder.get_object("log_info_bar").connect("response", lambda b, r: self._log_frame.set_visible(False))
# Search.
self._search_bar = builder.get_object("sat_update_search_bar")
self._search_bar.bind_property("search-mode-enabled", self._search_bar, "visible")
self._search_bar_box = builder.get_object("search_bar_box")
search_provider = SearchProvider(self._sat_view,
builder.get_object("sat_update_search_entry"),
builder.get_object("sat_update_search_down_button"),
builder.get_object("sat_update_search_up_button"))
builder.get_object("sat_update_find_button").connect("toggled", search_provider.on_search_toggled)
builder.get_object("search_button").connect("toggled", search_provider.on_search_toggled)
# Satellite lists init on dialog start.
self._sat_view.connect("realize", self.on_update_satellites_list)
# Options.
@@ -545,6 +546,8 @@ class UpdateDialog:
@run_idle
def append_satellites(self, sats):
model = get_base_model(self._sat_view.get_model())
if not model:
return
for sat in sats:
itr = model.append(sat)
@@ -597,10 +600,10 @@ class UpdateDialog:
self._sat_update_info_bar.set_visible(False)
def on_find_toggled(self, button: Gtk.ToggleToolButton):
self._search_bar.set_search_mode(button.get_active())
self._search_bar_box.set_visible(button.get_active())
def on_filter_toggled(self, button: Gtk.ToggleToolButton):
self._filter_bar.set_search_mode(button.get_active())
self._filter_bar_box.set_visible(button.get_active())
@run_idle
def on_filter(self, item):
@@ -684,8 +687,15 @@ class SatellitesUpdateDialog(UpdateDialog):
box.pack_start(Gtk.Label(translate("Merge satellites by positions")), False, True, 0)
box.pack_end(self._merge_sat_switch, False, True, 0)
self._general_options_box.pack_start(box, True, True, 0)
self._general_options_box.show_all()
self._split_band_switch = Gtk.Switch(active=self._dialog_settings.get("split_by_band", False))
self._split_band_switch.connect("state-set", lambda b, s: self._dialog_settings.update({"split_by_band": s}))
box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL)
box.pack_start(Gtk.Label(translate("Split satellites by bands (C/KU)")), False, True, 0)
box.pack_end(self._split_band_switch, False, True, 0)
self._general_options_box.pack_start(box, True, True, 0)
self._general_options_box.show_all()
self._skip_c_band_switch.get_parent().set_visible(False)
@run_idle
@@ -756,6 +766,28 @@ class SatellitesUpdateDialog(UpdateDialog):
else:
sats = {s.name: s for s in sats} # key = name, v = satellite
# Post-processing if band separation is active.
if self._split_band_switch.get_active():
appender.send(f"Checking and splitting satellites by band...\n")
to_remove = []
new_sats = {}
for name, sat in sats.items():
# Checking for C/KU-transponders.
c_tr = []
ku_tr = []
[c_tr.append(t) if int(t.frequency) < 10000000 else ku_tr.append(t) for t in sat.transponders]
if ku_tr and c_tr:
c_sat = Satellite(f"{name} (C)", sat.flags, sat.position, c_tr)
ku_sat = Satellite(f"{name} (KU)", sat.flags, sat.position, ku_tr)
new_sats[c_sat.name] = c_sat
new_sats[ku_sat.name] = ku_sat
to_remove.append(name)
[sats.pop(n) for n in to_remove]
sats.update(new_sats)
appender.send("-" * _len + "\n")
for row in self._main_model:
pos = row[0]
if pos in sats:
@@ -972,14 +1004,14 @@ class ServicesUpdateDialog(UpdateDialog):
no_lb = "No Category"
if self._kos_bq_groups_switch.get_active():
self.gen_bouquet_group(tv_services, tv_bouquets, lambda s: s[4] or no_lb)
self.gen_bouquet_group(rd_services, radio_bouquets, lambda s: s[4] or no_lb, bq_type=BqType.RADIO.value)
self.gen_bouquet_group(tv_services, tv_bouquets, lambda s: s[5] or no_lb)
self.gen_bouquet_group(rd_services, radio_bouquets, lambda s: s[5] or no_lb, bq_type=BqType.RADIO.value)
if self._kos_bq_lang_switch.get_active():
lb = "" if no_lb in {b.name for b in tv_bouquets} else "No Region"
self.gen_bouquet_group(tv_services, tv_bouquets, lambda s: s[5] or lb)
self.gen_bouquet_group(tv_services, tv_bouquets, lambda s: s[4] or lb)
lb = "" if no_lb in {b.name for b in radio_bouquets} else "No Region"
self.gen_bouquet_group(rd_services, radio_bouquets, lambda s: s[5] or lb, bq_type=BqType.RADIO.value)
self.gen_bouquet_group(rd_services, radio_bouquets, lambda s: s[4] or lb, bq_type=BqType.RADIO.value)
return Bouquets("", BqType.TV.value, tv_bouquets), Bouquets("", BqType.RADIO.value, radio_bouquets)

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2024 Dmitriy Yefremov
# Copyright (c) 2018-2025 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -580,14 +580,14 @@ class SatellitesTool(Gtk.Box):
def on_download(self, app, page):
if page is Page.SATELLITE:
self._app.on_download_data(DownloadType.SATELLITES)
self._app.on_download_data(DownloadType.SATELLITES, files_filter=(f"{self._dvb_type}.xml",))
def on_upload(self, app, page):
if page is Page.SATELLITE:
self._app.upload_data(DownloadType.SATELLITES)
self._app.upload_data(DownloadType.SATELLITES, files_filter=(f"{self._dvb_type}.xml",))
@run_idle
def on_update(self, item):
def on_update(self, item=None):
SatellitesUpdateDialog(self._app.get_active_window(), self._settings, self._satellite_view.get_model()).show()

View File

@@ -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">

View File

@@ -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>

View File

@@ -5,11 +5,17 @@ The best way to run this program from source is using of [MSYS2](https://www.msy
![mingw64](https://user-images.githubusercontent.com/7511379/161400639-898ceb10-7de8-4557-bde1-25fe32bdfb03.png)
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/`

View File

@@ -1,5 +1,5 @@
#!/bin/bash
VER="3.9.0_Beta"
VER="3.14.1_Beta"
B_PATH="dist/DemonEditor"
DEB_PATH="$B_PATH/usr/share/demoneditor"

View File

@@ -1,5 +1,5 @@
Package: demon-editor
Version: 3.9.0-Beta
Version: 3.14.1-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>

View File

@@ -5,7 +5,7 @@ Source: https://github.com/DYefremov/DemonEditor
Files: *
MIT License
Copyright (c) 2018-2024 Dmitriy Yefremov
Copyright (c) 2018-2025 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -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

View File

@@ -81,8 +81,8 @@ app = BUNDLE(coll,
'CFBundleGetInfoString': "Enigma2 channel and satellite editor",
'LSApplicationCategoryType': 'public.app-category.utilities',
'LSMinimumSystemVersion': '10.13',
'CFBundleShortVersionString': f"3.9.0.{BUILD_DATE} Beta",
'NSHumanReadableCopyright': u"Copyright © 2024, Dmitriy Yefremov",
'CFBundleShortVersionString': f"3.14.1.{BUILD_DATE} Beta",
'NSHumanReadableCopyright': u"Copyright © 2018-2025, Dmitriy Yefremov",
'NSRequiresAquaSystemAppearance': 'false',
'NSHighResolutionCapable': 'true'
})

View File

@@ -10,6 +10,7 @@ GenericName[nl]=Enigma2 boeket editor
GenericName[pl]=Edytor bukietów Enigma2
GenericName[pt]=Editor de buquês Enigma2
GenericName[ru]=Редактор букетов Enigma2
GenericName[sk]=Enigma2 editor balíčkov
GenericName[tr]=Enigma2 buket düzenleyici
GenericName[zh_CN]=Enigma2频道编辑器
Comment=Channel and satellite list editor for Enigma2
@@ -21,6 +22,7 @@ Comment[nl]=Kanaal- en satellietlijsteditor voor Enigma2
Comment[pl]=Edytor list kanałów i satelitów dla Enigma2
Comment[pt]=Editor de lista de canais e satélites para Enigma2
Comment[ru]=Редактор списка каналов и спутников для Enigma2
Comment[sk]=Editor zoznamu kanálov a satelitov pre Enigma2
Comment[tr]=Enigma2 için Kanal ve uydu listesi düzenleyici
Comment[zh_CN]=Enigma2频道和卫星列表编辑器
Icon=demon-editor

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 Dmitriy Yefremov
# Copyright (C) 2018-2025 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 "Dmitriy Yefremov"
# Main
msgid "Service"
@@ -258,8 +258,14 @@ msgid "Extra:"
msgstr "Дадаткова:"
# Filter bar
msgid "Only free"
msgstr "Толькі адкрытыя"
msgid "Access"
msgstr "Доступ"
msgid "Free (FTA)"
msgstr "Адкрытыя (FTA)"
msgid "Coded"
msgstr "Закадаваныя"
msgid "All positions"
msgstr "Усе пазіцыі"
@@ -332,8 +338,8 @@ msgstr "Шлях да піконаў фармату Enigma2:"
msgid "Specify the correct position value for the provider!"
msgstr "Пакажыце правільнае значэнне пазіцыі для правайдара!"
msgid "Converter between name formats"
msgstr "Канвертар фармату імёнаў"
msgid "Converter between formats"
msgstr "Канвертар фарматаў"
msgid "Receive picons for providers"
msgstr "Атрыманне піконаў для правайдараў"
@@ -1542,3 +1548,51 @@ 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 "Стварыць новы сэрвіс"

View File

@@ -1,8 +1,8 @@
# Copyright (C) 2018-2024 Dmitriy Yefremov
# Copyright (C) 2018-2025 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
# Charly, 2019.
# Dmitriy Yefremov, 2020-2024.
# Dmitriy Yefremov, 2020-2025.
# Thomas Schmidt, 2021.
msgid ""
msgstr ""
@@ -260,8 +260,14 @@ msgid "Extra:"
msgstr "Extra:"
# Filter bar
msgid "Only free"
msgstr "Nur freie"
msgid "Access"
msgstr "Zugriff"
msgid "Free (FTA)"
msgstr "Freie (FTA)"
msgid "Coded"
msgstr "Codiert"
msgid "All positions"
msgstr "Alle Positionen"
@@ -334,8 +340,8 @@ msgstr "Pfad zu Enigma2-Picons:"
msgid "Specify the correct position value for the provider!"
msgstr "Geben Sie den richtigen Positionswert für den Provider an!"
msgid "Converter between name formats"
msgstr "Konverter zwischen Namensformaten"
msgid "Converter between formats"
msgstr "Konverter zwischen Formaten"
msgid "Receive picons for providers"
msgstr "Picons für Provider erhalten"
@@ -1556,3 +1562,51 @@ 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"

View File

@@ -1,11 +1,11 @@
# Copyright (C) 2018-2024 Dmitriy Yefremov
# Copyright (C) 2018-2025 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
# SPDX-FileCopyrightText: 2022, 2023, 2024 Massimo Pissarello <mapi68@gmail.com>
# SPDX-FileCopyrightText: 2022, 2023, 2024, 2025 Massimo Pissarello <mapi68@gmail.com>
msgid ""
msgstr ""
"Project-Id-Version: \n"
"PO-Revision-Date: 2024-02-20 21:33+0100\n"
"PO-Revision-Date: 2025-08-30 09:41+0200\n"
"Last-Translator: Massimo Pissarello <mapi68@gmail.com>\n"
"Language-Team: Italian <>\n"
"Language: it\n"
@@ -13,12 +13,10 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Lokalize 23.08.4\n"
"X-Generator: Lokalize 25.08.0\n"
msgid "translator-credits"
msgstr ""
"Nicola Fanghella\n"
"Massimo Pissarello"
msgstr "Massimo Pissarello\nNicola Fanghella"
# Main
msgid "Service"
@@ -265,8 +263,14 @@ msgid "Extra:"
msgstr "Extra:"
# Filter bar
msgid "Only free"
msgstr "Solo gratis"
msgid "Access"
msgstr "Accesso"
msgid "Free (FTA)"
msgstr "Libero (FTA)"
msgid "Coded"
msgstr "Codificato"
msgid "All positions"
msgstr "Tutte le posizioni"
@@ -728,7 +732,7 @@ msgid "XML file"
msgstr "File XML"
msgid "Use web source"
msgstr "Usa fonte web"
msgstr "Usa sorgente web"
msgid "Url to *.xml.gz file:"
msgstr "Da URL a file *.xml.gz:"
@@ -824,8 +828,8 @@ msgstr "Abilita barra di riproduzione diretta"
msgid "Enables direct sending and playback of media links on the receiver"
msgstr ""
"Abilita l'invio e la riproduzione diretta di collegamenti multimediali sul"
" ricevitore"
"Abilita invio e la riproduzione diretta di collegamenti multimediali sul ricev"
"itore"
msgid "Watch the channel in the program"
msgstr "Guarda canale nel programma"
@@ -1312,7 +1316,7 @@ msgid "Add picons"
msgstr "Aggiungi picon"
msgid "Logs"
msgstr "Registri"
msgstr "Log"
msgid "Title"
msgstr "Titolo"
@@ -1324,7 +1328,7 @@ msgid "Length"
msgstr "Durata"
msgid "Additional source"
msgstr "Fonte aggiuntiva"
msgstr "Sorgente aggiuntiva"
msgid "Automatically set the name selected in the favorites list."
msgstr "Imposta automaticamente il nome selezionato nell'elenco dei preferiti."
@@ -1412,8 +1416,8 @@ msgstr "Riproduci dall'elenco principale"
msgid "Enables URL parsing using yt-dlp to get direct links to media."
msgstr ""
"Abilita l'analisi degli URL utilizzando yt-dlp per ottenere collegamenti"
" diretti ai contenuti multimediali."
"Abilita analisi degli URL utilizzando yt-dlp per ottenere collegamenti diretti"
" ai contenuti multimediali."
msgid "Permissions..."
msgstr "Permessi..."
@@ -1466,8 +1470,8 @@ msgid ""
" selected.\n"
" Recommended only if you have external storage."
msgstr ""
"Abilita il caricamento come archivio compresso se viene selezionato un numero"
" elevato di picon (> 1000). Consigliato solo se si dispone di memoria esterna."
"Abilita caricamento come archivio compresso se viene selezionato un numero ele"
"vato di picon (> 1000). Consigliato solo se si dispone di memoria esterna."
msgid "Clear \"New\" flag"
msgstr "Rimuovi il flag \"Nuovo\""
@@ -1509,11 +1513,10 @@ msgid "Removed"
msgstr "Rimosso"
msgid "Enables overwriting existing main list services."
msgstr ""
"Abilita la sovrascrittura dei servizi esistenti dell'elenco principale."
msgstr "Abilita sovrascrittura dei servizi esistenti dell'elenco principale."
msgid "Enables skipping services import from lamedb."
msgstr "Abilita il salto dell'importazione dei servizi da lamedb."
msgstr "Abilita salto dell'importazione dei servizi da lamedb."
msgid "Bouquets data only"
msgstr "Bouquet solo dati"
@@ -1585,13 +1588,13 @@ msgid "Current EPG cache contents."
msgstr "Contenuto attuale cache EPG."
msgid "Source error!"
msgstr "Errore fonte!"
msgstr "Errore sorgente!"
msgid "The EPG source for the favorites list is not set!"
msgstr "La fonte EPG per l'elenco dei preferiti non è impostata!"
msgstr "La sorgente EPG per l'elenco dei preferiti non è impostata!"
msgid "Add to EPG sources list"
msgstr "Aggiungi all'elenco delle fonti EPG"
msgstr "Aggiungi all'elenco delle sorgenti EPG"
msgid "Current bouquet"
msgstr "Bouquet attuale"
@@ -1604,3 +1607,56 @@ 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"

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 Dmitriy Yefremov
# Copyright (C) 2018-2025 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 "Dmitriy Yefremov"
# Main
msgid "Service"
@@ -258,8 +258,14 @@ msgid "Extra:"
msgstr "Дополнительно:"
# Filter bar
msgid "Only free"
msgstr "Только открытые"
msgid "Access"
msgstr "Доступ"
msgid "Free (FTA)"
msgstr "Oткрытые (FTA)"
msgid "Coded"
msgstr "Закодированные"
msgid "All positions"
msgstr "Все позиции"
@@ -332,8 +338,8 @@ msgstr "Путь к пиконам формата Enigma2:"
msgid "Specify the correct position value for the provider!"
msgstr "Укажите правильное значение позиции для провайдера!"
msgid "Converter between name formats"
msgstr "Конвертер формата имен"
msgid "Converter between formats"
msgstr "Конвертер форматов"
msgid "Receive picons for providers"
msgstr "Получение пиконов для провайдеров"
@@ -1539,3 +1545,51 @@ 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 "Создать новый сервис"

1603
po/sk/demon-editor.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: DemonEditor\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-16 15:59+0300\n"
"PO-Revision-Date: 2023-10-08 13:43+0300\n"
"PO-Revision-Date: 2025-08-24 12:50+0300\n"
"Last-Translator: audi06_19 <info@dreamosat-forum.com>\n"
"Language-Team: audi06_19 <info@dreamosat-forum.com>\n"
"Language: tr\n"
@@ -11,7 +11,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.3.2\n"
"X-Generator: Poedit 3.7\n"
msgid "translator-credits"
msgstr "audi06_19 <info@dreamosat-forum.com>"
@@ -261,8 +261,14 @@ msgid "Extra:"
msgstr "Ekstra:"
# Filter bar
msgid "Only free"
msgstr "Sadece ücretsiz"
msgid "Access"
msgstr "Erişim"
msgid "Free (FTA)"
msgstr "Açık (FTA)"
msgid "Coded"
msgstr "Kodlanmış"
msgid "All positions"
msgstr "Tüm pozisyonlar"
@@ -335,8 +341,8 @@ msgstr "Enigma2 piconların yolu:"
msgid "Specify the correct position value for the provider!"
msgstr "Sağlayıcı için doğru pozisyon değerini belirtin!"
msgid "Converter between name formats"
msgstr "İsim formatları arasında dönüştürücü"
msgid "Converter between formats"
msgstr "Biçimler arasında dönüştürücü"
msgid "Receive picons for providers"
msgstr "Sağlayıcılar için picon alma"
@@ -1534,3 +1540,90 @@ 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"