mirror of
https://github.com/DYefremov/DemonEditor.git
synced 2026-05-09 10:56:13 +02:00
Compare commits
99 Commits
1.0.2-b1-m
...
1.0.7-a1-w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a34798f215 | ||
|
|
d6738826d3 | ||
|
|
a437ec6030 | ||
|
|
9cc8605994 | ||
|
|
a972ee353f | ||
|
|
be2d64e480 | ||
|
|
76e732c8a0 | ||
|
|
e81e13a5c0 | ||
|
|
5b12777223 | ||
|
|
e0c953ee05 | ||
|
|
3dc4caf65d | ||
|
|
8308b715dd | ||
|
|
3a8831f0f9 | ||
|
|
a020a23211 | ||
|
|
a67c81235c | ||
|
|
3ef587841e | ||
|
|
55a21fbc18 | ||
|
|
b60a9a69b6 | ||
|
|
8b517f6f88 | ||
|
|
81dd12a038 | ||
|
|
17de78f169 | ||
|
|
3411f32868 | ||
|
|
5f669f4480 | ||
|
|
f56e4b616a | ||
|
|
653ef1422f | ||
|
|
9d5e07af1f | ||
|
|
399c1ff01b | ||
|
|
ad8e6975b1 | ||
|
|
5f79b27daa | ||
|
|
3b23ddc1a7 | ||
|
|
a0e3566bec | ||
|
|
1cada0408f | ||
|
|
e1a5b8e39d | ||
|
|
a46c6ae816 | ||
|
|
33137ee879 | ||
|
|
51181057b1 | ||
|
|
ca1e823bf1 | ||
|
|
7fb2d9ac4a | ||
|
|
f5a02ddf1d | ||
|
|
1266f8e04b | ||
|
|
2dcee99981 | ||
|
|
7053628e56 | ||
|
|
b8e1f0e7fd | ||
|
|
954f1c514a | ||
|
|
60e1f6c467 | ||
|
|
986f10c640 | ||
|
|
4c95972381 | ||
|
|
052dd3efbe | ||
|
|
4e867b6f22 | ||
|
|
c11278041e | ||
|
|
b89df3d65d | ||
|
|
d252c69628 | ||
|
|
6785e46745 | ||
|
|
bbffeaa30e | ||
|
|
ce11723d34 | ||
|
|
2cdefdca42 | ||
|
|
52b2bb28b4 | ||
|
|
672586e227 | ||
|
|
eaff4eec6c | ||
|
|
f068696aad | ||
|
|
f8eddd8710 | ||
|
|
a5206c89ef | ||
|
|
6555c3c882 | ||
|
|
155ed02f11 | ||
|
|
a1ce729ce2 | ||
|
|
33ffccf57a | ||
|
|
b9881fc345 | ||
|
|
35ce913ab0 | ||
|
|
29e1cb10a3 | ||
|
|
558843c728 | ||
|
|
c3534052ae | ||
|
|
1113fec26e | ||
|
|
2f55fb4e64 | ||
|
|
412a66e5e5 | ||
|
|
676bc14f73 | ||
|
|
ec6ebb2a0e | ||
|
|
f8710a4bf0 | ||
|
|
b48f638495 | ||
|
|
fd0559d76e | ||
|
|
6c6948ce23 | ||
|
|
573d755e31 | ||
|
|
912083f203 | ||
|
|
4df0553333 | ||
|
|
f0d535ba4e | ||
|
|
88ef5563cf | ||
|
|
6431f2ccd8 | ||
|
|
ca9b4a780d | ||
|
|
f74eead20b | ||
|
|
8d4d90fd9f | ||
|
|
4269d16d31 | ||
|
|
9cf3e97bd3 | ||
|
|
3b85d35b62 | ||
|
|
6bddd89206 | ||
|
|
e16f2cba82 | ||
|
|
59748aa9ba | ||
|
|
caf4925409 | ||
|
|
2ab540ccfa | ||
|
|
4b762802da | ||
|
|
41a6e54e90 |
@@ -1,67 +1,48 @@
|
||||
import os
|
||||
import datetime
|
||||
import distutils.util
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
EXE_NAME = 'start.py'
|
||||
DIR_PATH = os.getcwd()
|
||||
COMPILING_PLATFORM = distutils.util.get_platform()
|
||||
PATH_EXE = [os.path.join(DIR_PATH, EXE_NAME)]
|
||||
STRIP = True
|
||||
BUILD_DATE = datetime.datetime.now().strftime("%Y%m%d")
|
||||
|
||||
block_cipher = None
|
||||
|
||||
ui_files = [('app/ui/*.glade', 'ui'),
|
||||
('app/ui/*.css', 'ui'),
|
||||
('app/ui/*.ui', 'ui'),
|
||||
('app/ui/lang*', 'share/locale'),
|
||||
('app/ui/icons*', 'share/icons')
|
||||
ui_files = [('app\\ui\\*.glade', 'ui'),
|
||||
('app\\ui\\*.css', 'ui'),
|
||||
('app\\ui\\*.ui', 'ui'),
|
||||
('app\\ui\\lang*', 'share\\locale'),
|
||||
('app\\ui\\icons*', 'share\\icons')
|
||||
]
|
||||
|
||||
|
||||
a = Analysis([EXE_NAME],
|
||||
pathex=PATH_EXE,
|
||||
binaries=None,
|
||||
binaries=[],
|
||||
datas=ui_files,
|
||||
hiddenimports=['fileinput', 'uuid'],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=['youtube_dl', 'tkinter'],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
|
||||
pyz = PYZ(a.pure,
|
||||
a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='DemonEditor',
|
||||
debug=False,
|
||||
strip=STRIP,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False)
|
||||
|
||||
console=False, icon='icon.ico')
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=STRIP,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='DemonEditor')
|
||||
|
||||
app = BUNDLE(coll,
|
||||
name='DemonEditor.app',
|
||||
icon='icon.icns',
|
||||
bundle_identifier=None,
|
||||
info_plist={
|
||||
'NSPrincipalClass': 'NSApplication',
|
||||
'CFBundleName': 'DemonEditor',
|
||||
'CFBundleDisplayName': 'DemonEditor',
|
||||
'CFBundleGetInfoString': "Enigma2 channel and satellites editor",
|
||||
'LSApplicationCategoryType': 'public.app-category.utilities',
|
||||
'CFBundleShortVersionString': "1.0.0 Beta (Build: {})".format(BUILD_DATE),
|
||||
'NSHumanReadableCopyright': u"Copyright © 2020, Dmitriy Yefremov",
|
||||
'NSRequiresAquaSystemAppearance': 'false'
|
||||
})
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2021 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
|
||||
|
||||
90
README.md
90
README.md
@@ -1,78 +1,55 @@
|
||||
# <img src="app/ui/icons/hicolor/96x96/apps/demon-editor.png" width="32" /> DemonEditor
|
||||
[](LICENSE) 
|
||||
## Enigma2 channel and satellites list editor for macOS (experimental).
|
||||
[](LICENSE) 
|
||||
## Enigma2 channel and satellite list editor for MS Windows (experimental).
|
||||
|
||||
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
|
||||
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc).
|
||||
**The functionality and performance of this version may be different from the [Linux version](https://github.com/DYefremov/DemonEditor)!**
|
||||
|
||||

|
||||
|
||||
## Main features of the program
|
||||
* Editing bouquets, channels, satellites.
|
||||
* Import function.
|
||||
* Backup function.
|
||||
* Extended support of IPTV.
|
||||
* Support of picons.
|
||||
* Downloading of picons and updating of satellites (transponders) from the web.
|
||||
* Importing services, downloading picons and updating satellites from the Web.
|
||||
* Import to bouquet(Neutrino WEBTV) from m3u.
|
||||
* Export of bouquets with IPTV services in m3u.
|
||||
* Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental).
|
||||
* Preview (playback) of IPTV or other streams directly from the bouquet list (should be installed [VLC](https://www.videolan.org/vlc/)).
|
||||
#### Keyboard shortcuts
|
||||
* **⌘ + X** - only in bouquet list.
|
||||
* **⌘ + C** - only in services list.
|
||||
Clipboard is **"rubber"**. There is an accumulation before the insertion!
|
||||
* **⌘ + E** - edit.
|
||||
* **⌘ + R, F2** - rename.
|
||||
* **⌘ + S, T** in Satellites edit tool for create satellite or transponder.
|
||||
* **⌘ + L** - parental lock.
|
||||
* **⌘ + H** - hide/skip.
|
||||
* **⌘ + P** - start play IPTV or other stream in the bouquet list.
|
||||
* **⌘ + Z** - switch(**zap**) the channel(works when the HTTP API is enabled, Enigma2 only).
|
||||
* **⌘ + W** - switch to the channel and watch in the program.
|
||||
* **⌘ + Up/Down** - move selected items in the list.
|
||||
* **⌘ + O** - (re)load user data from current dir.
|
||||
* **⌘ + D** - load data from receiver.
|
||||
* **⌘ + U/B** - upload data/bouquets to receiver.
|
||||
* **⌘ + F** - show/hide search bar.
|
||||
* **⇧ + ⌘ + F** - show/hide filter bar.
|
||||
* **Left/Right** - remove selection.
|
||||
* Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
|
||||
* Simple FTP client (experimental).
|
||||
|
||||
For **multiple** selection with the mouse, press and hold the **⌘** key!
|
||||
#### Keyboard shortcuts
|
||||
* **Ctrl + X** - only in bouquet list.
|
||||
* **Ctrl + C** - only in services list.
|
||||
Clipboard is **"rubber"**. There is an accumulation before the insertion!
|
||||
* **Ctrl + Insert** - copies the selected channels from the main list to the bouquet
|
||||
beginning or inserts (creates) a new bouquet.
|
||||
* **Ctrl + BackSpace** - copies the selected channels from the main list to the bouquet end.
|
||||
* **Ctrl + E** - edit.
|
||||
* **Ctrl + R, F2** - rename.
|
||||
* **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.
|
||||
* **Space** - select/deselect.
|
||||
* **Left/Right** - remove selection.
|
||||
* **Ctrl + Up, Down, PageUp, PageDown, Home, End**- move selected items in the list.
|
||||
* **Ctrl + O** - (re)load user data from current dir.
|
||||
* **Ctrl + D** - load data from receiver.
|
||||
* **Ctrl + U/B** - upload data/bouquets to receiver.
|
||||
* **Ctrl + I** - extra info, details.
|
||||
* **Ctrl + F** - show/hide search bar.
|
||||
* **Ctrl + Shift + F** - show/hide filter bar.
|
||||
|
||||
For **multiple** selection with the mouse, press and hold the **Ctrl** key!
|
||||
|
||||
## Minimum requirements
|
||||
*Python >= 3.5.2, GTK+ >= 3.16 with PyGObject bindings, python3-requests.*
|
||||
*Python >= 3.4.4, GTK+ >= 3.16 with PyGObject bindings, python3-requests.*
|
||||
|
||||
## Installation and Launch
|
||||
To run the program on macOS, you need to install [brew](https://brew.sh/).
|
||||
Then install the required components via terminal:
|
||||
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme```
|
||||
```pip3 install requests```
|
||||
#### Optional:
|
||||
```brew install wget```
|
||||
```pip3 install pillow, pyobjc```
|
||||
|
||||
To start the program, just download the [archive](https://github.com/DYefremov/DemonEditor/archive/experimental-mac.zip), unpack and run it from the terminal
|
||||
with the command: ```./start.py```
|
||||
## Standalone package
|
||||
You can also download the ready-made package as a ***.dmg** file from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
|
||||
Recommended copy the package to the **Application** directory.
|
||||
Perhaps in the security settings it will be necessary to allow the launch of this application!
|
||||
**The package may not contain all the latest changes. Not all features can be supported and tested!**
|
||||
|
||||
THIS SOFTWARE COMES WITH ABSOLUTELY NO WARRANTY.
|
||||
AUTHOR IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY CONNECTION WITH THIS SOFTWARE.
|
||||
The package may contain components distributed under the GPL [v3](http://www.gnu.org/licenses/gpl-3.0.html) or lower license.
|
||||
By downloading and using this package you agree to the terms of this [license](http://www.gnu.org/licenses/gpl-3.0.html) and the possible inconvenience associated with this!
|
||||
|
||||
#### Building your own package
|
||||
Install [PyInstaller](https://www.pyinstaller.org/) with the command from the terminal:
|
||||
|
||||
```pip3 install pyinstaller```
|
||||
|
||||
and in the root dir run command:
|
||||
|
||||
```pyinstaller DemonEditor.spec```
|
||||
## Important
|
||||
**This version is not fully tested and has experimental status!**
|
||||
|
||||
@@ -84,6 +61,7 @@ When using the multiple import feature, from *lamedb* will be taken data **only
|
||||
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.
|
||||
|
||||
#### Command line arguments:
|
||||
* **-l** - write logs to file.
|
||||
* **-d on/off** - turn on/off debug mode. Allows to display more information in the logs.
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
theme: jekyll-theme-slate
|
||||
show_downloads: true
|
||||
title: DemonEditor
|
||||
description: Enigma2 channel and satellite list editor.
|
||||
show_downloads: false
|
||||
@@ -67,7 +67,7 @@ def run_with_delay(timeout=5):
|
||||
timer.cancel()
|
||||
|
||||
def run():
|
||||
GLib.idle_add(func, *args, **kwargs, priority=GLib.PRIORITY_LOW)
|
||||
GLib.idle_add(func, priority=GLib.PRIORITY_LOW, *args, **kwargs)
|
||||
|
||||
timer = Timer(interval=timeout, function=run)
|
||||
timer.start()
|
||||
|
||||
@@ -5,8 +5,7 @@ import time
|
||||
import urllib
|
||||
import xml.etree.ElementTree as ETree
|
||||
from enum import Enum
|
||||
from ftplib import FTP, error_perm
|
||||
from http.client import RemoteDisconnected
|
||||
from ftplib import FTP, CRLF, Error, error_perm
|
||||
from telnetlib import Telnet
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlencode
|
||||
@@ -43,8 +42,289 @@ class HttpApiException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def download_data(*, settings, download_type=DownloadType.ALL, callback=print, files_filter=None):
|
||||
with FTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
|
||||
class UtfFTP(FTP):
|
||||
""" FTP class wrapper. """
|
||||
|
||||
def retrlines(self, cmd, callback=None):
|
||||
""" Small modification of the original method.
|
||||
|
||||
It is used to retrieve data in line mode and skip errors related
|
||||
to reading file names in encoding other than UTF-8 or Latin-1.
|
||||
Decode errors are ignored [UnicodeDecodeError, etc].
|
||||
"""
|
||||
if callback is None:
|
||||
callback = log
|
||||
self.sendcmd("TYPE A")
|
||||
with self.transfercmd(cmd) as conn, conn.makefile("r", encoding=self.encoding, errors="ignore") as fp:
|
||||
while 1:
|
||||
line = fp.readline(self.maxline + 1)
|
||||
if len(line) > self.maxline:
|
||||
msg = "UtfFTP [retrlines] error: got more than {} bytes".format(self.maxline)
|
||||
log(msg)
|
||||
raise Error(msg)
|
||||
if self.debugging > 2:
|
||||
log('UtfFTP [retrlines] *retr* {}'.format(repr(line)))
|
||||
if not line:
|
||||
break
|
||||
if line[-2:] == CRLF:
|
||||
line = line[:-2]
|
||||
elif line[-1:] == "\n":
|
||||
line = line[:-1]
|
||||
callback(line)
|
||||
return self.voidresp()
|
||||
|
||||
# ***************** Download ******************* #
|
||||
|
||||
def download_files(self, save_path, file_list, callback=None):
|
||||
""" Downloads files from the receiver via FTP. """
|
||||
for file in filter(lambda s: s.endswith(file_list), self.nlst()):
|
||||
self.download_file(file, save_path, callback)
|
||||
|
||||
def download_file(self, name, save_path, callback=None):
|
||||
with open(save_path + name, "wb") as f:
|
||||
msg = "Downloading file: {}. Status: {}\n"
|
||||
try:
|
||||
resp = str(self.retrbinary("RETR " + name, f.write))
|
||||
except error_perm as e:
|
||||
resp = str(e)
|
||||
msg = msg.format(name, e)
|
||||
log(msg.rstrip())
|
||||
else:
|
||||
msg = msg.format(name, resp)
|
||||
|
||||
callback(msg) if callback else log(msg.rstrip())
|
||||
|
||||
return resp
|
||||
|
||||
def download_dir(self, path, save_path, callback=None):
|
||||
""" Downloads directory from FTP with all contents.
|
||||
|
||||
Creates a leaf directory and all intermediate ones. This is recursive.
|
||||
"""
|
||||
os.makedirs(os.path.join(save_path, path), exist_ok=True)
|
||||
|
||||
files = []
|
||||
self.dir(path, files.append)
|
||||
for f in files:
|
||||
f_data = f.split()
|
||||
f_path = os.path.join(path, " ".join(f_data[8:]))
|
||||
|
||||
if f_data[0][0] == "d":
|
||||
try:
|
||||
os.makedirs(os.path.join(save_path, f_path), exist_ok=True)
|
||||
except OSError as e:
|
||||
msg = "Download dir error: {}".format(e).rstrip()
|
||||
log(msg)
|
||||
return "500 " + msg
|
||||
else:
|
||||
self.download_dir(f_path, save_path, callback)
|
||||
else:
|
||||
try:
|
||||
self.download_file(f_path, save_path, callback)
|
||||
except OSError as e:
|
||||
log("Download dir error: {}".format(e).rstrip())
|
||||
|
||||
resp = "226 Transfer complete."
|
||||
msg = "Copy directory {}. Status: {}".format(path, resp)
|
||||
log(msg)
|
||||
|
||||
if callback:
|
||||
callback(msg)
|
||||
|
||||
return resp
|
||||
|
||||
def download_xml(self, data_path, xml_path, xml_files, callback):
|
||||
""" Used for download *.xml files. """
|
||||
self.cwd(xml_path)
|
||||
self.download_files(data_path, xml_files, callback)
|
||||
|
||||
def download_picons(self, src, dest, callback, files_filter=None):
|
||||
try:
|
||||
self.cwd(src)
|
||||
except error_perm as e:
|
||||
callback(str(e))
|
||||
return
|
||||
|
||||
for file in filter(picons_filter_function(files_filter), self.nlst()):
|
||||
self.download_file(file, dest, callback)
|
||||
|
||||
# ***************** Uploading ******************* #
|
||||
|
||||
def upload_bouquets(self, data_path, remove_unused, callback):
|
||||
if remove_unused:
|
||||
self.remove_unused_bouquets(callback)
|
||||
self.upload_files(data_path, BQ_FILES_LIST, callback)
|
||||
|
||||
def upload_files(self, data_path, file_list, callback):
|
||||
for file_name in os.listdir(data_path):
|
||||
if file_name in STC_XML_FILE or file_name in WEB_TV_XML_FILE:
|
||||
continue
|
||||
if file_name.endswith(file_list):
|
||||
self.send_file(file_name, data_path, callback)
|
||||
|
||||
def upload_xml(self, data_path, xml_path, xml_files, callback):
|
||||
""" Used for transfer *.xml files. """
|
||||
self.cwd(xml_path)
|
||||
for xml_file in xml_files:
|
||||
self.send_file(xml_file, data_path, callback)
|
||||
|
||||
def upload_picons(self, src, dest, callback, files_filter=None):
|
||||
try:
|
||||
self.cwd(dest)
|
||||
except error_perm as e:
|
||||
if str(e).startswith("550"):
|
||||
self.mkd(dest) # if not exist
|
||||
self.cwd(dest)
|
||||
|
||||
for file_name in filter(picons_filter_function(files_filter), os.listdir(src)):
|
||||
self.send_file(file_name, src, callback)
|
||||
|
||||
def remove_unused_bouquets(self, callback):
|
||||
bq_files = ("userbouquet.", "bouquets.xml", "ubouquets.xml")
|
||||
|
||||
for file in filter(lambda f: f.startswith(bq_files), self.nlst()):
|
||||
self.delete_file(file, callback)
|
||||
|
||||
def send_file(self, file_name, path, callback=None):
|
||||
""" Opens the file in binary mode and transfers into receiver """
|
||||
file_src = path + file_name
|
||||
resp = "500"
|
||||
if not os.path.isfile(file_src):
|
||||
log("Uploading file: '{}'. File not found. Skipping.".format(file_src))
|
||||
return resp + " File not found."
|
||||
|
||||
with open(file_src, "rb") as f:
|
||||
msg = "Uploading file: {}. Status: {}\n"
|
||||
try:
|
||||
resp = str(self.storbinary("STOR " + file_name, f))
|
||||
except Error as e:
|
||||
resp = str(e)
|
||||
msg = msg.format(file_name, resp)
|
||||
log(msg)
|
||||
else:
|
||||
msg = msg.format(file_name, resp)
|
||||
|
||||
if callback:
|
||||
callback(msg)
|
||||
|
||||
return resp
|
||||
|
||||
def upload_dir(self, path, callback=None):
|
||||
""" Uploads directory to FTP with all contents.
|
||||
|
||||
Creates a leaf directory and all intermediate ones. This is recursive.
|
||||
"""
|
||||
resp = "200"
|
||||
msg = "Uploading directory: {}. Status: {}"
|
||||
try:
|
||||
files = os.listdir(path)
|
||||
except OSError as e:
|
||||
log(e)
|
||||
else:
|
||||
os.chdir(path)
|
||||
for f in files:
|
||||
file = r"{}{}".format(path, f)
|
||||
if os.path.isfile(file):
|
||||
self.send_file(f, path, callback)
|
||||
elif os.path.isdir(file):
|
||||
try:
|
||||
self.mkd(f)
|
||||
except Error:
|
||||
pass # NOP
|
||||
|
||||
try:
|
||||
self.cwd(f)
|
||||
except Error as e:
|
||||
resp = str(e)
|
||||
log(msg.format(f, resp))
|
||||
else:
|
||||
self.upload_dir(file + "/")
|
||||
|
||||
self.cwd("..")
|
||||
os.chdir("..")
|
||||
|
||||
if callback:
|
||||
callback(msg.format(path, resp))
|
||||
|
||||
return resp
|
||||
|
||||
# ****************** Deletion ******************** #
|
||||
|
||||
def delete_picons(self, callback, dest=None, files_filter=None):
|
||||
if dest:
|
||||
try:
|
||||
self.cwd(dest)
|
||||
except Error as e:
|
||||
callback(str(e))
|
||||
return
|
||||
|
||||
for file in filter(picons_filter_function(files_filter), self.nlst()):
|
||||
self.delete_file(file, callback)
|
||||
|
||||
def delete_file(self, file, callback=log):
|
||||
msg = "Deleting file: {}. Status: {}\n"
|
||||
try:
|
||||
resp = self.delete(file)
|
||||
except Error as e:
|
||||
resp = str(e)
|
||||
msg = msg.format(file, resp)
|
||||
log(msg)
|
||||
else:
|
||||
msg = msg.format(file, resp)
|
||||
|
||||
if callback:
|
||||
callback(msg)
|
||||
|
||||
return resp
|
||||
|
||||
def delete_dir(self, path, callback=None):
|
||||
files = []
|
||||
self.dir(path, files.append)
|
||||
for f in files:
|
||||
f_data = f.split()
|
||||
name = " ".join(f_data[8:])
|
||||
f_path = path + "/" + name
|
||||
|
||||
if f_data[0][0] == "d":
|
||||
self.delete_dir(f_path, callback)
|
||||
else:
|
||||
self.delete_file(f_path, callback)
|
||||
|
||||
msg = "Remove directory {}. Status: {}\n"
|
||||
try:
|
||||
resp = self.rmd(path)
|
||||
except Error as e:
|
||||
msg = msg.format(path, e)
|
||||
log(msg)
|
||||
return "500"
|
||||
else:
|
||||
msg = msg.format(path, resp)
|
||||
log(msg.rstrip())
|
||||
|
||||
if callback:
|
||||
callback(msg)
|
||||
|
||||
return resp
|
||||
|
||||
def rename_file(self, from_name, to_name, callback=None):
|
||||
msg = "File rename: {}. Status: {}\n"
|
||||
try:
|
||||
resp = self.rename(from_name, to_name)
|
||||
except Error as e:
|
||||
resp = str(e)
|
||||
msg = msg.format(from_name, resp)
|
||||
log(msg)
|
||||
else:
|
||||
msg = msg.format(from_name, resp)
|
||||
|
||||
if callback:
|
||||
callback(msg)
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
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:
|
||||
ftp.encoding = "utf-8"
|
||||
callback("FTP OK.\n")
|
||||
save_path = settings.data_local_path
|
||||
@@ -53,17 +333,17 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=print, f
|
||||
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
|
||||
ftp.cwd(settings.services_path)
|
||||
file_list = BQ_FILES_LIST + DATA_FILES_LIST if download_type is DownloadType.ALL else BQ_FILES_LIST
|
||||
download_files(ftp, save_path, file_list, callback)
|
||||
ftp.download_files(save_path, file_list, callback)
|
||||
# *.xml and webtv
|
||||
if download_type in (DownloadType.ALL, DownloadType.SATELLITES):
|
||||
download_xml(ftp, save_path, settings.satellites_xml_path, STC_XML_FILE, callback)
|
||||
ftp.download_xml(save_path, settings.satellites_xml_path, STC_XML_FILE, callback)
|
||||
if download_type in (DownloadType.ALL, DownloadType.WEBTV):
|
||||
download_xml(ftp, save_path, settings.satellites_xml_path, WEB_TV_XML_FILE, callback)
|
||||
ftp.download_xml(save_path, settings.satellites_xml_path, WEB_TV_XML_FILE, callback)
|
||||
|
||||
if download_type is DownloadType.PICONS:
|
||||
picons_path = settings.picons_local_path
|
||||
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
|
||||
download_picons(ftp, settings.picons_path, picons_path, callback, files_filter)
|
||||
ftp.download_picons(settings.picons_path, picons_path, callback, files_filter)
|
||||
# epg.dat
|
||||
if download_type is DownloadType.EPG:
|
||||
stb_path = settings.services_path
|
||||
@@ -73,13 +353,13 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=print, f
|
||||
save_path = epg_options.get("epg_dat_path", save_path)
|
||||
|
||||
ftp.cwd(stb_path)
|
||||
download_files(ftp, save_path, "epg.dat", callback)
|
||||
ftp.download_files(save_path, "epg.dat", callback)
|
||||
|
||||
callback("\nDone.\n")
|
||||
|
||||
|
||||
def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False,
|
||||
callback=print, done_callback=None, use_http=False, files_filter=None):
|
||||
callback=log, done_callback=None, use_http=False, files_filter=None):
|
||||
s_type = settings.setting_type
|
||||
data_path = settings.data_local_path
|
||||
host = settings.host
|
||||
@@ -89,7 +369,7 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
|
||||
|
||||
try:
|
||||
if s_type is SettingsType.ENIGMA_2 and use_http:
|
||||
ht = http(settings.http_user, settings.http_password, base_url, callback, settings.http_use_ssl)
|
||||
ht = http(settings.user, settings.password, base_url, callback, settings.http_use_ssl)
|
||||
next(ht)
|
||||
message = ""
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
@@ -112,8 +392,8 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
|
||||
if download_type is not DownloadType.PICONS:
|
||||
# telnet
|
||||
tn = telnet(host=host,
|
||||
user=settings.telnet_user,
|
||||
password=settings.telnet_password,
|
||||
user=settings.user,
|
||||
password=settings.password,
|
||||
timeout=settings.telnet_timeout)
|
||||
next(tn)
|
||||
# terminate enigma or neutrino
|
||||
@@ -121,33 +401,33 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
|
||||
tn.send("init 4")
|
||||
callback("Stopping GUI...\n")
|
||||
|
||||
with FTP(host=host, user=settings.user, passwd=settings.password) as ftp:
|
||||
with UtfFTP(host=host, user=settings.user, passwd=settings.password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
callback("FTP OK.\n")
|
||||
sat_xml_path = settings.satellites_xml_path
|
||||
services_path = settings.services_path
|
||||
|
||||
if download_type is DownloadType.SATELLITES:
|
||||
upload_xml(ftp, data_path, sat_xml_path, STC_XML_FILE, callback)
|
||||
ftp.upload_xml(data_path, sat_xml_path, STC_XML_FILE, callback)
|
||||
|
||||
if s_type is SettingsType.NEUTRINO_MP and download_type is DownloadType.WEBTV:
|
||||
upload_xml(ftp, data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
|
||||
ftp.upload_xml(data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
|
||||
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
ftp.cwd(services_path)
|
||||
upload_bouquets(ftp, data_path, remove_unused, callback)
|
||||
ftp.upload_bouquets(data_path, remove_unused, callback)
|
||||
|
||||
if download_type is DownloadType.ALL:
|
||||
upload_xml(ftp, data_path, sat_xml_path, STC_XML_FILE, callback)
|
||||
ftp.upload_xml(data_path, sat_xml_path, STC_XML_FILE, callback)
|
||||
if s_type is SettingsType.NEUTRINO_MP:
|
||||
upload_xml(ftp, data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
|
||||
ftp.upload_xml(data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
|
||||
|
||||
ftp.cwd(services_path)
|
||||
upload_bouquets(ftp, data_path, remove_unused, callback)
|
||||
upload_files(ftp, data_path, DATA_FILES_LIST, callback)
|
||||
ftp.upload_bouquets(data_path, remove_unused, callback)
|
||||
ftp.upload_files(data_path, DATA_FILES_LIST, callback)
|
||||
|
||||
if download_type is DownloadType.PICONS:
|
||||
upload_picons(ftp, settings.picons_local_path, settings.picons_path, callback, files_filter)
|
||||
ftp.upload_picons(settings.picons_local_path, settings.picons_path, callback, files_filter)
|
||||
|
||||
if tn and not use_http:
|
||||
# resume enigma or restart neutrino
|
||||
@@ -169,90 +449,13 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
|
||||
ht.close()
|
||||
|
||||
|
||||
def upload_bouquets(ftp, data_path, remove_unused, callback):
|
||||
if remove_unused:
|
||||
remove_unused_bouquets(ftp, callback)
|
||||
upload_files(ftp, data_path, BQ_FILES_LIST, callback)
|
||||
|
||||
|
||||
def upload_files(ftp, data_path, file_list, callback):
|
||||
for file_name in os.listdir(data_path):
|
||||
if file_name in STC_XML_FILE or file_name in WEB_TV_XML_FILE:
|
||||
continue
|
||||
if file_name.endswith(file_list):
|
||||
send_file(file_name, data_path, ftp, callback)
|
||||
|
||||
|
||||
def remove_unused_bouquets(ftp, callback):
|
||||
files = []
|
||||
ftp.dir(files.append)
|
||||
bq_files = ("tv", "radio", "bouquets.xml", "ubouquets.xml")
|
||||
|
||||
for file in filter(lambda f: f.endswith(bq_files), map(lambda f: f.split()[-1], map(str.rstrip, files))):
|
||||
callback("Deleting file: {}. Status: {}\n".format(file, ftp.delete(file)))
|
||||
|
||||
|
||||
def upload_xml(ftp, data_path, xml_path, xml_files, callback):
|
||||
""" Used for transfer *.xml files. """
|
||||
ftp.cwd(xml_path)
|
||||
for xml_file in xml_files:
|
||||
send_file(xml_file, data_path, ftp, callback)
|
||||
|
||||
|
||||
def download_xml(ftp, data_path, xml_path, xml_files, callback):
|
||||
""" Used for download *.xml files. """
|
||||
ftp.cwd(xml_path)
|
||||
download_files(ftp, data_path, xml_files, callback)
|
||||
|
||||
|
||||
# ***************** Picons *******************#
|
||||
|
||||
def upload_picons(ftp, src, dest, callback, files_filter=None):
|
||||
try:
|
||||
ftp.cwd(dest)
|
||||
except error_perm as e:
|
||||
if str(e).startswith("550"):
|
||||
ftp.mkd(dest) # if not exist
|
||||
ftp.cwd(dest)
|
||||
|
||||
for file_name in filter(picons_filter_function(files_filter), os.listdir(src)):
|
||||
send_file(file_name, src, ftp, callback)
|
||||
|
||||
|
||||
def download_picons(ftp, src, dest, callback, files_filter=None):
|
||||
try:
|
||||
ftp.cwd(src)
|
||||
except error_perm as e:
|
||||
callback(str(e))
|
||||
return
|
||||
|
||||
files = []
|
||||
ftp.dir(files.append)
|
||||
|
||||
for file in filter(picons_filter_function(files_filter), map(lambda f: f.split()[-1], map(str.rstrip, files))):
|
||||
download_file(ftp, file, dest, callback)
|
||||
|
||||
|
||||
def delete_picons(ftp, callback, dest=None, files_filter=None):
|
||||
if dest:
|
||||
try:
|
||||
ftp.cwd(dest)
|
||||
except error_perm as e:
|
||||
callback(str(e))
|
||||
return
|
||||
|
||||
files = []
|
||||
ftp.dir(files.append)
|
||||
|
||||
for file in filter(picons_filter_function(files_filter), map(lambda f: f.split()[-1], map(str.rstrip, files))):
|
||||
callback("Delete file: {}. Status: {}\n".format(file, ftp.delete(file)))
|
||||
|
||||
|
||||
def remove_picons(*, settings, callback, done_callback=None, files_filter=None):
|
||||
with FTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
|
||||
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
callback("FTP OK.\n")
|
||||
delete_picons(ftp, callback, settings.picons_path, files_filter)
|
||||
ftp.delete_picons(callback, settings.picons_path, files_filter)
|
||||
if done_callback:
|
||||
done_callback()
|
||||
|
||||
@@ -261,31 +464,6 @@ def picons_filter_function(files_filter=None):
|
||||
return lambda f: f in files_filter if files_filter else f.endswith(PICONS_SUF)
|
||||
|
||||
|
||||
def download_files(ftp, save_path, file_list, callback):
|
||||
""" Downloads files from the receiver via FTP. """
|
||||
files = []
|
||||
ftp.dir(files.append)
|
||||
|
||||
for file in map(lambda f: f.split()[-1], filter(lambda s: s.endswith(file_list), map(str.rstrip, files))):
|
||||
download_file(ftp, file, save_path, callback)
|
||||
|
||||
|
||||
def download_file(ftp, name, save_path, callback):
|
||||
with open(save_path + name, "wb") as f:
|
||||
callback("Downloading file: {}. Status: {}\n".format(name, str(ftp.retrbinary("RETR " + name, f.write))))
|
||||
|
||||
|
||||
def send_file(file_name, path, ftp, callback):
|
||||
""" Opens the file in binary mode and transfers into receiver """
|
||||
file_src = path + file_name
|
||||
if not os.path.isfile(file_src):
|
||||
log("Uploading file: '{}'. File not found. Skipping.".format(file_src))
|
||||
return
|
||||
|
||||
with open(file_src, "rb") as f:
|
||||
callback("Uploading file: {}. Status: {}\n".format(file_name, str(ftp.storbinary("STOR " + file_name, f))))
|
||||
|
||||
|
||||
def http(user, password, url, callback, use_ssl=False):
|
||||
init_auth(user, password, url, use_ssl)
|
||||
data = get_post_data(url, password, url)
|
||||
@@ -420,7 +598,7 @@ class HttpAPI:
|
||||
|
||||
@run_task
|
||||
def init(self):
|
||||
user, password = self._settings.http_user, self._settings.http_password
|
||||
user, password = self._settings.user, self._settings.password
|
||||
use_ssl = self._settings.http_use_ssl
|
||||
self._main_url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port)
|
||||
self._base_url = "{}/web/".format(self._main_url)
|
||||
@@ -475,7 +653,7 @@ def get_response(req_type, url, data=None):
|
||||
if req_type is HttpAPI.Request.TEST:
|
||||
raise e
|
||||
return {"error_code": e.code}
|
||||
except (URLError, RemoteDisconnected, ConnectionResetError) as e:
|
||||
except (URLError, ConnectionResetError) as e:
|
||||
if req_type is HttpAPI.Request.TEST:
|
||||
raise e
|
||||
except ETree.ParseError as e:
|
||||
@@ -534,7 +712,7 @@ def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message
|
||||
|
||||
try:
|
||||
return get_response(HttpAPI.Request.TEST, "{}/web/{}".format(base_url, params), data).get("e2statetext", "")
|
||||
except (RemoteDisconnected, URLError, HTTPError) as e:
|
||||
except (URLError, HTTPError) as e:
|
||||
raise TestException(e)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,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 get_bouquets as get_enigma_bouquets, write_bouquets as write_enigma_bouquets, to_bouquet_id
|
||||
from .enigma.bouquets import to_bouquet_id, 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
|
||||
@@ -27,7 +27,7 @@ def write_services(path, channels, s_type, format_version):
|
||||
|
||||
def get_bouquets(path, s_type):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
return get_enigma_bouquets(path)
|
||||
return BouquetsReader(path).get()
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
return get_neutrino_bouquets(path)
|
||||
|
||||
@@ -35,7 +35,7 @@ def get_bouquets(path, s_type):
|
||||
@run_task
|
||||
def write_bouquets(path, bouquets, s_type, force_bq_names=False):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
write_enigma_bouquets(path, bouquets, force_bq_names)
|
||||
BouquetsWriter(path, bouquets, force_bq_names).write()
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
write_neutrino_bouquets(path, bouquets)
|
||||
|
||||
|
||||
@@ -14,9 +14,11 @@ class BqServiceType(Enum):
|
||||
IPTV = "IPTV"
|
||||
MARKER = "MARKER" # 64
|
||||
SPACE = "SPACE" # 832 [hidden marker]
|
||||
ALT = "ALT" # Service with alternatives
|
||||
|
||||
|
||||
Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden"])
|
||||
Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden", "file"])
|
||||
Bouquet.__new__.__defaults__ = (None, BqServiceType.DEFAULT, [], None, None, None) # For Python3 < 3.7
|
||||
Bouquets = namedtuple("Bouquets", ["name", "type", "bouquets"])
|
||||
BouquetService = namedtuple("BouquetService", ["name", "type", "data", "num"])
|
||||
|
||||
@@ -33,6 +35,7 @@ class TrType(Enum):
|
||||
Satellite = "s"
|
||||
Terrestrial = "t"
|
||||
Cable = "c"
|
||||
ATSC = "a"
|
||||
|
||||
|
||||
class BqType(Enum):
|
||||
@@ -145,6 +148,10 @@ T_SYSTEM = {"0": "DVB-T", "1": "DVB-T2", "-1": "DVB-T/T2"}
|
||||
# Cable
|
||||
C_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256"}
|
||||
|
||||
# ATSC
|
||||
A_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256", "6": "8VSB",
|
||||
"7": "16VSB"}
|
||||
|
||||
# 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"}
|
||||
|
||||
@@ -9,14 +9,15 @@ __FILE_NAME = "blacklist"
|
||||
|
||||
def get_blacklist(path):
|
||||
with suppress(FileNotFoundError):
|
||||
with open(path + __FILE_NAME, "r") as file:
|
||||
with open(path + __FILE_NAME, "r", encoding="utf-8") as file:
|
||||
# filter empty values and "\n"
|
||||
return {*list(filter(None, (x.strip() for x in file.readlines())))}
|
||||
return {}
|
||||
|
||||
return set(filter(None, (x.strip() for x in file.readlines())))
|
||||
return set()
|
||||
|
||||
|
||||
def write_blacklist(path, channels):
|
||||
with open(path + __FILE_NAME, "w") as file:
|
||||
with open(path + __FILE_NAME, "w", encoding="utf-8") as file:
|
||||
if channels:
|
||||
file.writelines("\n".join(channels))
|
||||
|
||||
|
||||
@@ -1,76 +1,196 @@
|
||||
""" Module for working with Enigma2 bouquets. """
|
||||
import re
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
from app.commons import log
|
||||
from app.eparser.ecommons import BqServiceType, BouquetService, Bouquets, Bouquet, BqType
|
||||
|
||||
_TV_ROOT_FILE_NAME = "bouquets.tv"
|
||||
_RADIO_ROOT_FILE_NAME = "bouquets.radio"
|
||||
_TV_FILE = "bouquets.tv"
|
||||
_RADIO_FILE = "bouquets.radio"
|
||||
_DEFAULT_BOUQUET_NAME = "favourites"
|
||||
|
||||
|
||||
def get_bouquets(path):
|
||||
return parse_bouquets(path, "bouquets.tv", BqType.TV.value), parse_bouquets(path, "bouquets.radio",
|
||||
BqType.RADIO.value)
|
||||
|
||||
|
||||
def write_bouquets(path, bouquets, force_bq_names=False):
|
||||
""" Creating and writing bouquets files.
|
||||
class BouquetsWriter:
|
||||
""" Class for creating and writing bouquet files..
|
||||
|
||||
If "force_bq_names" then naming the files using the name of the bouquet.
|
||||
Some images may have problems displaying the favorites list!
|
||||
"""
|
||||
srv_line = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
|
||||
line = []
|
||||
pattern = re.compile("[^\\w_()]+")
|
||||
m_index = [0]
|
||||
s_index = [0]
|
||||
_SERVICE = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
|
||||
_MARKER = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n"
|
||||
_SPACE = "#SERVICE 1:832:D:{}:0:0:0:0:0:0:\n"
|
||||
_ALT = '#SERVICE 1:134:1:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet\n'
|
||||
_ALT_PAT = r"[<>:\"/\\|?*\-\s]"
|
||||
|
||||
for bqs in bouquets:
|
||||
line.clear()
|
||||
line.append("#NAME {}\n".format(bqs.name))
|
||||
def __init__(self, path, bouquets, force_bq_names=False):
|
||||
self._path = path
|
||||
self._bouquets = bouquets
|
||||
self._force_bq_names = force_bq_names
|
||||
self._marker_index = 1
|
||||
self._space_index = 0
|
||||
self._alt_names = set()
|
||||
|
||||
for index, bq in enumerate(bqs.bouquets):
|
||||
bq_name = bq.name
|
||||
if bq_name == "Favourites (TV)" or bq_name == "Favourites (Radio)":
|
||||
bq_name = _DEFAULT_BOUQUET_NAME
|
||||
def write(self):
|
||||
line = []
|
||||
pattern = re.compile("[^\\w_()]+")
|
||||
|
||||
for bqs in self._bouquets:
|
||||
line.clear()
|
||||
line.append("#NAME {}\n".format(bqs.name))
|
||||
bq_file_names = {b.file for b in bqs.bouquets}
|
||||
count = 1
|
||||
|
||||
for bq in bqs.bouquets:
|
||||
bq_name = bq.file
|
||||
if not bq_name:
|
||||
if self._force_bq_names:
|
||||
bq_name = re.sub(pattern, "_", bq.name)
|
||||
else:
|
||||
bq_name = "de{0:02d}".format(count)
|
||||
while bq_name in bq_file_names:
|
||||
count += 1
|
||||
bq_name = "de{0:02d}".format(count)
|
||||
bq_file_names.add(bq_name)
|
||||
|
||||
line.append(self._SERVICE.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
|
||||
self.write_bouquet(self._path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services)
|
||||
|
||||
with open(self._path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
|
||||
file.writelines(line)
|
||||
|
||||
def write_bouquet(self, path, name, services):
|
||||
""" Writes single bouquet file. """
|
||||
bouquet = ["#NAME {}\n".format(name)]
|
||||
for srv in services:
|
||||
s_type = srv.service_type
|
||||
if s_type == BqServiceType.IPTV.name:
|
||||
bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip()))
|
||||
elif s_type == BqServiceType.MARKER.name:
|
||||
m_data = srv.fav_id.strip().split(":")
|
||||
m_data[2] = self._marker_index
|
||||
self._marker_index += 1
|
||||
bouquet.append(self._MARKER.format(m_data[2], m_data[-1]))
|
||||
elif s_type == BqServiceType.SPACE.name:
|
||||
bouquet.append(self._SPACE.format(self._space_index))
|
||||
self._space_index += 1
|
||||
elif s_type == BqServiceType.ALT.name:
|
||||
services = srv.transponder
|
||||
if services:
|
||||
p = Path(path)
|
||||
alt_name = srv.data_id
|
||||
f_name = "alternatives.{}{}".format(alt_name, p.suffix)
|
||||
|
||||
if self._force_bq_names:
|
||||
alt_name = re.sub(self._ALT_PAT, "_", srv.service).lower()
|
||||
f_name = "alternatives.{}{}".format(alt_name, p.suffix)
|
||||
|
||||
alt_path = "{}/{}".format(p.parent, f_name)
|
||||
bouquet.append(self._ALT.format(f_name))
|
||||
self.write_bouquet(alt_path, srv.service, services)
|
||||
else:
|
||||
bq_name = re.sub(pattern, "_", bq.name) if force_bq_names else "de{0:02d}".format(index)
|
||||
line.append(srv_line.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
|
||||
write_bouquet(path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services, m_index, s_index)
|
||||
data = to_bouquet_id(srv)
|
||||
if srv.service:
|
||||
bouquet.append("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, srv.service, srv.service))
|
||||
else:
|
||||
bouquet.append("#SERVICE {}\n".format(data))
|
||||
|
||||
with open(path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
|
||||
file.writelines(line)
|
||||
with open(path, "w", encoding="utf-8") as file:
|
||||
file.writelines(bouquet)
|
||||
|
||||
|
||||
def write_bouquet(path, name, services, current_marker, current_space):
|
||||
bouquet = ["#NAME {}\n".format(name)]
|
||||
marker = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n"
|
||||
space = "#SERVICE 1:832:D:{}:0:0:0:0:0:0:\n"
|
||||
class BouquetsReader:
|
||||
""" Class for reading and parsing bouquets. """
|
||||
_ALT_PAT = re.compile(".*alternatives\\.+(.*)\\.([tv|radio]+).*")
|
||||
_BQ_PAT = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*")
|
||||
_STREAM_TYPES = {"4097", "5001", "5002", "8193", "8739"}
|
||||
|
||||
for srv in services:
|
||||
s_type = srv.service_type
|
||||
__slots__ = ["_path"]
|
||||
|
||||
if s_type == BqServiceType.IPTV.name:
|
||||
bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip()))
|
||||
elif s_type == BqServiceType.MARKER.name:
|
||||
m_data = srv.fav_id.strip().split(":")
|
||||
m_data[2] = current_marker[0]
|
||||
current_marker[0] += 1
|
||||
bouquet.append(marker.format(m_data[2], m_data[-1]))
|
||||
elif s_type == BqServiceType.SPACE.name:
|
||||
bouquet.append(space.format(current_space[0]))
|
||||
current_space[0] += 1
|
||||
else:
|
||||
data = to_bouquet_id(srv)
|
||||
if srv.service:
|
||||
bouquet.append("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, srv.service, srv.service))
|
||||
else:
|
||||
bouquet.append("#SERVICE {}\n".format(data))
|
||||
def __init__(self, path):
|
||||
self._path = path
|
||||
|
||||
with open(path, "w", encoding="utf-8") as file:
|
||||
file.writelines(bouquet)
|
||||
def get(self):
|
||||
""" Returns a tuple of TV and Radio bouquets. """
|
||||
return self.parse_bouquets(_TV_FILE, BqType.TV.value), self.parse_bouquets(_RADIO_FILE, BqType.RADIO.value)
|
||||
|
||||
def parse_bouquets(self, bq_name, bq_type):
|
||||
with open(self._path + bq_name, encoding="utf-8", errors="replace") as file:
|
||||
lines = file.readlines()
|
||||
bouquets = None
|
||||
nm_sep = "#NAME"
|
||||
b_names = set()
|
||||
real_b_names = Counter()
|
||||
|
||||
for line in lines:
|
||||
if nm_sep in line:
|
||||
_, _, name = line.partition(nm_sep)
|
||||
bouquets = Bouquets(name.strip(), bq_type, [])
|
||||
if bouquets and "#SERVICE" in line:
|
||||
name = re.match(self._BQ_PAT, line)
|
||||
if name:
|
||||
b_name = name.group(1)
|
||||
if b_name in b_names:
|
||||
log("The list of bouquets contains duplicate [{}] names!".format(b_name))
|
||||
else:
|
||||
b_names.add(b_name)
|
||||
|
||||
rb_name, services = self.get_bouquet(self._path, b_name, bq_type)
|
||||
if rb_name in real_b_names:
|
||||
log("Bouquet file 'userbouquet.{}.{}' has duplicate name: {}".format(b_name, bq_type,
|
||||
rb_name))
|
||||
real_b_names[rb_name] += 1
|
||||
rb_name = "{} {}".format(rb_name, real_b_names[rb_name])
|
||||
else:
|
||||
real_b_names[rb_name] = 0
|
||||
|
||||
bouquets[2].append(Bouquet(rb_name, bq_type, services, None, None, b_name))
|
||||
else:
|
||||
raise ValueError("No bouquet name found for: {}".format(line))
|
||||
|
||||
return bouquets
|
||||
|
||||
@staticmethod
|
||||
def get_bouquet(path, bq_name, bq_type, prefix="userbouquet"):
|
||||
""" Parsing services ids from bouquet file. """
|
||||
with open(path + "{}.{}.{}".format(prefix, bq_name, bq_type), 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("Bouquet file 'userbouquet.{}.{}' is empty or wrong!".format(bq_name, bq_type))
|
||||
return "{} [empty]".format(bq_name), services
|
||||
|
||||
bq_name = srvs.pop(0)
|
||||
|
||||
for num, srv in enumerate(srvs, start=1):
|
||||
srv_data = srv.strip().split(":")
|
||||
s_type = srv_data[1]
|
||||
if s_type == "64":
|
||||
m_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num))
|
||||
elif s_type == "832":
|
||||
m_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
|
||||
elif s_type == "134":
|
||||
alt = re.match(BouquetsReader._ALT_PAT, srv)
|
||||
if alt:
|
||||
alt_name, alt_type = alt.group(1), alt.group(2)
|
||||
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, alt_name, alt_type, "alternatives")
|
||||
services.append(BouquetService(alt_bq_name, BqServiceType.ALT, alt_name, tuple(alt_srvs)))
|
||||
elif srv_data[0].strip() in BouquetsReader._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 = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6])
|
||||
name = None
|
||||
if len(srv_data) == 12:
|
||||
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")
|
||||
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num))
|
||||
|
||||
return bq_name.lstrip("#NAME").strip(), services
|
||||
|
||||
|
||||
def to_bouquet_id(srv):
|
||||
@@ -82,81 +202,5 @@ def to_bouquet_id(srv):
|
||||
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, srv.fav_id)
|
||||
|
||||
|
||||
def get_bouquet(path, bq_name, bq_type):
|
||||
""" Parsing services ids from bouquet file. """
|
||||
with open(path + "userbouquet.{}.{}".format(bq_name, bq_type), 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("Bouquet file 'userbouquet.{}.{}' is empty or wrong!".format(bq_name, bq_type))
|
||||
return "{} [empty]".format(bq_name), services
|
||||
|
||||
bq_name = srvs.pop(0)
|
||||
|
||||
for num, srv in enumerate(srvs, start=1):
|
||||
srv_data = srv.strip().split(":")
|
||||
if srv_data[1] == "64":
|
||||
m_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num))
|
||||
elif srv_data[1] == "832":
|
||||
m_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
|
||||
elif "http" in srv or srv_data[0] == "8193":
|
||||
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 = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6])
|
||||
name = None
|
||||
if len(srv_data) == 12:
|
||||
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")
|
||||
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num))
|
||||
|
||||
return bq_name.lstrip("#NAME").strip(), services
|
||||
|
||||
|
||||
def parse_bouquets(path, bq_name, bq_type):
|
||||
with open(path + bq_name, encoding="utf-8", errors="replace") as file:
|
||||
lines = file.readlines()
|
||||
bouquets = None
|
||||
nm_sep = "#NAME"
|
||||
bq_pattern = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*")
|
||||
b_names = set()
|
||||
real_b_names = Counter()
|
||||
|
||||
for line in lines:
|
||||
if nm_sep in line:
|
||||
_, _, name = line.partition(nm_sep)
|
||||
bouquets = Bouquets(name.strip(), bq_type, [])
|
||||
if bouquets and "#SERVICE" in line:
|
||||
name = re.match(bq_pattern, line)
|
||||
if name:
|
||||
b_name = name.group(1)
|
||||
if b_name in b_names:
|
||||
log("The list of bouquets contains duplicate [{}] names!".format(b_name))
|
||||
else:
|
||||
b_names.add(b_name)
|
||||
|
||||
rb_name, services = get_bouquet(path, b_name, bq_type)
|
||||
if rb_name in real_b_names:
|
||||
log("Bouquet file 'userbouquet.{}.{}' has duplicate name: {}".format(b_name, bq_type, rb_name))
|
||||
real_b_names[rb_name] += 1
|
||||
rb_name = "{} {}".format(rb_name, real_b_names[rb_name])
|
||||
else:
|
||||
real_b_names[rb_name] = 0
|
||||
|
||||
bouquets[2].append(Bouquet(name=rb_name,
|
||||
type=bq_type,
|
||||
services=services,
|
||||
locked=None,
|
||||
hidden=None))
|
||||
else:
|
||||
raise ValueError("No bouquet name found for: {}".format(line))
|
||||
|
||||
return bouquets
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -13,282 +13,303 @@ _END_LINE = "# File was created in DemonEditor.\n# ....Enjoy watching!....\n"
|
||||
|
||||
|
||||
def get_services(path, format_version):
|
||||
return parse(path, format_version)
|
||||
return LameDbReader(path, format_version).parse()
|
||||
|
||||
|
||||
def write_services(path, services, format_version=4):
|
||||
if format_version == 4:
|
||||
write_to_lamedb(path, services)
|
||||
elif format_version == 5:
|
||||
write_to_lamedb5(path, services)
|
||||
LameDbWriter(path, services, format_version).write()
|
||||
|
||||
|
||||
def write_to_lamedb(path, services):
|
||||
""" Writing lamedb file ver.4 """
|
||||
with open(path + _FILE_NAME, "w") as file:
|
||||
file.writelines(get_services_lines(services))
|
||||
class LameDbReader:
|
||||
""" Lamedb parser class.
|
||||
|
||||
Reads and parses the Enigma2 lamedb[5] file.
|
||||
Supports versions 3, 4 and 5..
|
||||
"""
|
||||
__slots__ = ["_path", "_fmt"]
|
||||
|
||||
def __init__(self, path, fmt=4):
|
||||
self._path = path
|
||||
self._fmt = fmt
|
||||
|
||||
def parse(self):
|
||||
""" Parsing lamedb. """
|
||||
if self._fmt == 4:
|
||||
return self.parse_v4()
|
||||
elif self._fmt == 5:
|
||||
return self.parse_v5()
|
||||
raise SyntaxError("Unsupported version of the format.")
|
||||
|
||||
def parse_v3(self, services, transponders):
|
||||
""" Parsing version 3. """
|
||||
for t in transponders:
|
||||
tr = transponders[t].lower()
|
||||
tr_type = tr[0:1]
|
||||
if tr_type == "c":
|
||||
tr += ":0:0:0"
|
||||
elif tr_type == "t" or tr_type == "a":
|
||||
tr += ":0:0"
|
||||
else:
|
||||
tr_data = tr.split(_SEP)
|
||||
len_data = len(tr_data)
|
||||
if len_data == 6:
|
||||
tr_data.append("0")
|
||||
elif len_data == 9:
|
||||
tr_data.insert(6, "0")
|
||||
tr_data.append("0")
|
||||
tr_data.append("2")
|
||||
|
||||
tr = _SEP.join(tr_data)
|
||||
|
||||
transponders[t] = tr
|
||||
|
||||
return self.parse_services(services, transponders)
|
||||
|
||||
def parse_v4(self):
|
||||
""" Parsing version 4. """
|
||||
with open(self._path + _FILE_NAME, "r", encoding="utf-8", errors="replace") as file:
|
||||
try:
|
||||
data = str(file.read())
|
||||
except UnicodeDecodeError as e:
|
||||
log("lamedb parse error: " + str(e))
|
||||
else:
|
||||
return self.get_services_list(data)
|
||||
|
||||
def parse_v5(self):
|
||||
""" Parsing version 5. """
|
||||
with open(self._path + "lamedb5", "r", encoding="utf-8", errors="replace") as file:
|
||||
lns = file.readlines()
|
||||
|
||||
if lns and not lns[0].endswith("/5/\n"):
|
||||
raise SyntaxError("lamedb ver.5 parsing error: unsupported format.")
|
||||
|
||||
trs, srvs = {}, [""]
|
||||
for line in lns:
|
||||
if line.startswith("s:"):
|
||||
srv_data = line.strip("s:").split(",", 2)
|
||||
srv_data[1] = srv_data[1].strip("\"")
|
||||
data_len = len(srv_data)
|
||||
if data_len == 3:
|
||||
srv_data[2] = srv_data[2].strip()
|
||||
elif data_len == 2:
|
||||
srv_data.append("p:")
|
||||
srvs.extend(srv_data)
|
||||
elif line.startswith("t:"):
|
||||
data = line.split(",")
|
||||
len_data = len(data)
|
||||
if len_data > 1:
|
||||
tr, srv = data[0].strip("t:"), data[1].strip().replace(":", " ", 1)
|
||||
trs[tr] = srv
|
||||
else:
|
||||
log("Error while parsing transponder data [ver. 5] for line: {}".format(line))
|
||||
|
||||
return self.parse_services(srvs, trs)
|
||||
|
||||
def parse_services(self, services, transponders):
|
||||
""" Parsing services. """
|
||||
services_list = []
|
||||
blacklist = get_blacklist(self._path) if self._path else {}
|
||||
srvs = self.split(services, 3)
|
||||
if srvs[0][0] == "": # Remove first empty element.
|
||||
srvs.remove(srvs[0])
|
||||
|
||||
for srv in srvs:
|
||||
data_id = str(srv[0]).lower() # Lower is for lamedb ver.3.
|
||||
data = data_id.split(_SEP)
|
||||
sp = "0"
|
||||
tid = data[2]
|
||||
nid = data[3]
|
||||
# For lamedb ver.3
|
||||
is_v3 = False
|
||||
if len(tid) < 4:
|
||||
is_v3 = True
|
||||
tid = "{:0>4}".format(tid)
|
||||
data[2] = tid
|
||||
if len(nid) < 4:
|
||||
is_v3 = True
|
||||
nid = "{:0>4}".format(nid)
|
||||
data[3] = nid
|
||||
if is_v3:
|
||||
data[0] = "{:0>4}".format(data[0])
|
||||
data_id = _SEP.join(data)
|
||||
|
||||
srv_type = int(data[4])
|
||||
transponder_id = "{}:{}:{}".format(data[1], tid, nid)
|
||||
transponder = transponders.get(transponder_id, None)
|
||||
|
||||
tid = tid.lstrip(sp).upper()
|
||||
nid = nid.lstrip(sp).upper()
|
||||
ssid = str(data[0]).lstrip(sp).upper()
|
||||
onid = str(data[1]).lstrip(sp).upper()
|
||||
# For comparison in bouquets. Needed in upper case!!!
|
||||
fav_id = "{}:{}:{}:{}".format(ssid, tid, nid, onid)
|
||||
picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(srv_type, ssid, tid, nid, onid)
|
||||
s_id = "1:0:{:X}:{}:{}:{}:{}:0:0:0:".format(srv_type, ssid, tid, nid, onid)
|
||||
|
||||
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(int(flags[0][2:])) else None
|
||||
locked = LOCKED_ICON if s_id in blacklist else None
|
||||
|
||||
package = list(filter(lambda x: x.startswith("p:"), all_flags))
|
||||
package = package[0][2:] if package else ""
|
||||
|
||||
if transponder is not None:
|
||||
tr_type, sp, tr = str(transponder).partition(" ")
|
||||
tr_type = TrType(tr_type)
|
||||
tr = tr.split(_SEP)
|
||||
service_type = SERVICE_TYPE.get(data[4], SERVICE_TYPE["-2"])
|
||||
# Removing all non printable symbols!
|
||||
srv_name = "".join(c for c in srv[1] if c.isprintable())
|
||||
freq = tr[0]
|
||||
rate = tr[1]
|
||||
pol = None
|
||||
fec = None
|
||||
system = None
|
||||
pos = None
|
||||
|
||||
if tr_type is TrType.Satellite:
|
||||
pol = POLARIZATION.get(tr[2], None)
|
||||
fec = FEC.get(tr[3], None)
|
||||
system = "DVB-S2" if len(tr) > 7 else "DVB-S"
|
||||
pos = tr[4]
|
||||
if tr_type is TrType.Terrestrial:
|
||||
system = T_SYSTEM.get(tr[9], None)
|
||||
pos = "T"
|
||||
fec = T_FEC.get(tr[3], None)
|
||||
elif tr_type is TrType.Cable:
|
||||
system = "DVB-C"
|
||||
pos = "C"
|
||||
fec = FEC_DEFAULT.get(tr[4])
|
||||
elif tr_type is TrType.ATSC:
|
||||
system = "ATSC"
|
||||
pos = "T"
|
||||
fec = FEC_DEFAULT.get("0")
|
||||
|
||||
# Formatting displayed values.
|
||||
try:
|
||||
freq = "{}".format(int(freq) // 1000)
|
||||
rate = "{}".format(int(rate) // 1000)
|
||||
if tr_type is TrType.Satellite:
|
||||
pos = int(pos)
|
||||
pos = "{:0.1f}{}".format(abs(pos / 10), "W" if pos < 0 else "E")
|
||||
except ValueError as e:
|
||||
log("Parse error [parse_services]: {}".format(e))
|
||||
|
||||
s = Service(srv[2], tr_type.value, coded, srv_name, locked, hide, package, service_type, None,
|
||||
picon_id, data[0], freq, rate, pol, fec, system, pos, data_id, fav_id, transponder)
|
||||
|
||||
services_list.append(s)
|
||||
return services_list
|
||||
|
||||
def get_services_list(self, data):
|
||||
""" Returns a list of services from a string data representation. """
|
||||
transponders, sep, services = data.partition("transponders") # 1 step
|
||||
pattern = re.compile("/[34]/$")
|
||||
match = re.search(pattern, transponders)
|
||||
if not match:
|
||||
msg = "lamedb parsing error: unsupported format."
|
||||
log(msg)
|
||||
raise SyntaxError(msg)
|
||||
|
||||
transponders, sep, services = services.partition("services") # 2 step
|
||||
services, sep, _ = services.partition("\nend") # 3 step
|
||||
|
||||
if match.group() == "/3/":
|
||||
return self.parse_v3(services.split("\n"), self.parse_transponders(transponders.split("/")))
|
||||
|
||||
return self.parse_services(services.split("\n"), self.parse_transponders(transponders.split("/")))
|
||||
|
||||
@staticmethod
|
||||
def get_services_lines(services):
|
||||
""" Returns a list of strings from services for lamedb [v.4]. """
|
||||
lines = [_HEADER.format(4), "\ntransponders\n"]
|
||||
tr_lines = []
|
||||
services_lines = ["end\nservices\n"]
|
||||
tr_set = set()
|
||||
for srv in services:
|
||||
data_id = str(srv.data_id).split(_SEP)
|
||||
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
|
||||
if tr_id not in tr_set:
|
||||
transponder = "{}\n\t{}\n/\n".format(tr_id, srv.transponder)
|
||||
tr_lines.append(transponder)
|
||||
tr_set.add(tr_id)
|
||||
# Services
|
||||
services_lines.append("{}\n{}\n{}\n".format(srv.data_id, srv.service, srv.flags_cas))
|
||||
|
||||
tr_lines.sort()
|
||||
lines.extend(tr_lines)
|
||||
lines.extend(services_lines)
|
||||
lines.append("end\n" + _END_LINE)
|
||||
|
||||
return lines
|
||||
|
||||
def parse_transponders(self, arg):
|
||||
""" Parsing transponders. """
|
||||
transponders = {}
|
||||
for ar in arg:
|
||||
tr = ar.replace("\n", "").split("\t")
|
||||
if len(tr) == 2:
|
||||
transponders[tr[0]] = tr[1]
|
||||
|
||||
return transponders
|
||||
|
||||
def split(self, itr, size):
|
||||
""" Divide the iterable. """
|
||||
srv = []
|
||||
tmp = []
|
||||
for i, line in enumerate(itr):
|
||||
tmp.append(line)
|
||||
if i % size == 0:
|
||||
srv.append(tuple(tmp))
|
||||
tmp.clear()
|
||||
|
||||
return srv
|
||||
|
||||
|
||||
def get_services_lines(services):
|
||||
""" Returns a list of strings from services for lamedb [v.4]. """
|
||||
lines = [_HEADER.format(4), "\ntransponders\n"]
|
||||
tr_lines = []
|
||||
services_lines = ["end\nservices\n"]
|
||||
tr_set = set()
|
||||
for srv in services:
|
||||
data_id = str(srv.data_id).split(_SEP)
|
||||
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
|
||||
if tr_id not in tr_set:
|
||||
transponder = "{}\n\t{}\n/\n".format(tr_id, srv.transponder)
|
||||
tr_lines.append(transponder)
|
||||
tr_set.add(tr_id)
|
||||
# Services
|
||||
services_lines.append("{}\n{}\n{}\n".format(srv.data_id, srv.service, srv.flags_cas))
|
||||
class LameDbWriter:
|
||||
""" Writes the Enigma2 lamedb[5] file.
|
||||
|
||||
tr_lines.sort()
|
||||
lines.extend(tr_lines)
|
||||
lines.extend(services_lines)
|
||||
lines.append("end\n" + _END_LINE)
|
||||
Version 4 will be used instead of version 3!
|
||||
"""
|
||||
__slots__ = ["_path", "_fmt", "_services"]
|
||||
|
||||
return lines
|
||||
def __init__(self, path, services, fmt=4):
|
||||
self._path = path
|
||||
self._fmt = fmt
|
||||
self._services = services
|
||||
|
||||
def write(self):
|
||||
if self._fmt == 4:
|
||||
# Writing lamedb file ver.4
|
||||
with open(self._path + _FILE_NAME, "w", encoding="utf-8") as file:
|
||||
file.writelines(LameDbReader.get_services_lines(self._services))
|
||||
elif self._fmt == 5:
|
||||
self.write_to_lamedb5()
|
||||
|
||||
def write_to_lamedb5(path, services):
|
||||
""" Writing lamedb5 file. """
|
||||
lines = [_HEADER.format(5) + "\n"]
|
||||
services_lines = []
|
||||
tr_set = set()
|
||||
def write_to_lamedb5(self):
|
||||
""" Writing lamedb5 file. """
|
||||
lines = [_HEADER.format(5) + "\n"]
|
||||
services_lines = []
|
||||
tr_set = set()
|
||||
|
||||
for srv in services:
|
||||
data_id = str(srv.data_id).split(_SEP)
|
||||
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
|
||||
tr_set.add("t:{},{}\n".format(tr_id, srv.transponder.replace(" ", ":", 1)))
|
||||
# Removing empty packages
|
||||
flags = list(filter(lambda x: x != "p:", srv.flags_cas.split(",")))
|
||||
flags = ",".join(flags)
|
||||
flags = "," + flags if flags else ""
|
||||
services_lines.append("s:{},\"{}\"{}\n".format(srv.data_id, srv.service, flags))
|
||||
for srv in self._services:
|
||||
data_id = str(srv.data_id).split(_SEP)
|
||||
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
|
||||
tr_set.add("t:{},{}\n".format(tr_id, srv.transponder.replace(" ", ":", 1)))
|
||||
# Removing empty packages
|
||||
flags = list(filter(lambda x: x != "p:", srv.flags_cas.split(",")))
|
||||
flags = ",".join(flags)
|
||||
flags = "," + flags if flags else ""
|
||||
services_lines.append("s:{},\"{}\"{}\n".format(srv.data_id, srv.service, flags))
|
||||
|
||||
lines.extend(sorted(tr_set))
|
||||
lines.extend(services_lines)
|
||||
lines.append(_END_LINE)
|
||||
lines.extend(sorted(tr_set))
|
||||
lines.extend(services_lines)
|
||||
lines.append(_END_LINE)
|
||||
|
||||
with open(path + "lamedb5", "w") as file:
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
def parse(path, version=4):
|
||||
""" Parsing lamedb. """
|
||||
if version == 4:
|
||||
return parse_v4(path)
|
||||
elif version == 5:
|
||||
return parse_v5(path)
|
||||
raise SyntaxError("Unsupported version of the format.")
|
||||
|
||||
|
||||
def parse_v3(services, transponders, path):
|
||||
""" Parsing version 3. """
|
||||
for t in transponders:
|
||||
tr = transponders[t].lower()
|
||||
tr_type = tr[0:1]
|
||||
if tr_type == "c":
|
||||
tr += ":0:0:0"
|
||||
elif tr_type == "t":
|
||||
tr += ":0:0"
|
||||
else:
|
||||
tr_data = tr.split(_SEP)
|
||||
len_data = len(tr_data)
|
||||
if len_data == 6:
|
||||
tr_data.append("0")
|
||||
elif len_data == 9:
|
||||
tr_data.insert(6, "0")
|
||||
tr_data.append("0")
|
||||
tr_data.append("2")
|
||||
|
||||
tr = _SEP.join(tr_data)
|
||||
|
||||
transponders[t] = tr
|
||||
|
||||
return parse_services(services, transponders, path)
|
||||
|
||||
|
||||
def parse_v4(path):
|
||||
""" Parsing version 4. """
|
||||
with open(path + _FILE_NAME, "r", encoding="utf-8", errors="replace") as file:
|
||||
try:
|
||||
data = str(file.read())
|
||||
except UnicodeDecodeError as e:
|
||||
log("lamedb parse error: " + str(e))
|
||||
else:
|
||||
return get_services_list(data, path)
|
||||
|
||||
|
||||
def get_services_list(data, path=None):
|
||||
""" Returns a list of services from a string data representation. """
|
||||
transponders, sep, services = data.partition("transponders") # 1 step
|
||||
pattern = re.compile("/[34]/$")
|
||||
match = re.search(pattern, transponders)
|
||||
if not match:
|
||||
msg = "lamedb parsing error: unsupported format."
|
||||
log(msg)
|
||||
raise SyntaxError(msg)
|
||||
|
||||
transponders, sep, services = services.partition("services") # 2 step
|
||||
services, sep, _ = services.partition("\nend") # 3 step
|
||||
|
||||
if match.group() == "/3/":
|
||||
return parse_v3(services.split("\n"), parse_transponders(transponders.split("/")), path)
|
||||
|
||||
return parse_services(services.split("\n"), parse_transponders(transponders.split("/")), path)
|
||||
|
||||
|
||||
def parse_v5(path):
|
||||
""" Parsing version 5. """
|
||||
with open(path + "lamedb5", "r", encoding="utf-8", errors="replace") as file:
|
||||
lns = file.readlines()
|
||||
|
||||
if lns and not lns[0].endswith("/5/\n"):
|
||||
raise SyntaxError("lamedb v.5 parsing error: unsupported format.")
|
||||
|
||||
trs, srvs = {}, [""]
|
||||
for line in lns:
|
||||
if line.startswith("s:"):
|
||||
srv_data = line.strip("s:").split(",", 2)
|
||||
srv_data[1] = srv_data[1].strip("\"")
|
||||
data_len = len(srv_data)
|
||||
if data_len == 3:
|
||||
srv_data[2] = srv_data[2].strip()
|
||||
elif data_len == 2:
|
||||
srv_data.append("p:")
|
||||
srvs.extend(srv_data)
|
||||
elif line.startswith("t:"):
|
||||
tr, srv = line.split(",")
|
||||
trs[tr.strip("t:")] = srv.strip().replace(":", " ", 1)
|
||||
|
||||
return parse_services(srvs, trs, path)
|
||||
|
||||
|
||||
def parse_transponders(arg):
|
||||
""" Parsing transponders. """
|
||||
transponders = {}
|
||||
for ar in arg:
|
||||
tr = ar.replace("\n", "").split("\t")
|
||||
if len(tr) == 2:
|
||||
transponders[tr[0]] = tr[1]
|
||||
|
||||
return transponders
|
||||
|
||||
|
||||
def parse_services(services, transponders, path):
|
||||
""" Parsing services. """
|
||||
services_list = []
|
||||
blacklist = get_blacklist(path) if path else {}
|
||||
srvs = split(services, 3)
|
||||
if srvs[0][0] == "": # remove first empty element
|
||||
srvs.remove(srvs[0])
|
||||
|
||||
for srv in srvs:
|
||||
data_id = str(srv[0]).lower() # lower is for lamedb ver.3
|
||||
data = data_id.split(_SEP)
|
||||
sp = "0"
|
||||
tid = data[2]
|
||||
nid = data[3]
|
||||
# For lamedb ver.3
|
||||
is_v3 = False
|
||||
if len(tid) < 4:
|
||||
is_v3 = True
|
||||
tid = "{:0>4}".format(tid)
|
||||
data[2] = tid
|
||||
if len(nid) < 4:
|
||||
is_v3 = True
|
||||
nid = "{:0>4}".format(nid)
|
||||
data[3] = nid
|
||||
if is_v3:
|
||||
data[0] = "{:0>4}".format(data[0])
|
||||
data_id = _SEP.join(data)
|
||||
|
||||
srv_type = int(data[4])
|
||||
transponder_id = "{}:{}:{}".format(data[1], tid, nid)
|
||||
transponder = transponders.get(transponder_id, None)
|
||||
|
||||
tid = tid.lstrip(sp).upper()
|
||||
nid = nid.lstrip(sp).upper()
|
||||
ssid = str(data[0]).lstrip(sp).upper()
|
||||
onid = str(data[1]).lstrip(sp).upper()
|
||||
# For comparison in bouquets. Needed in upper case!!!
|
||||
fav_id = "{}:{}:{}:{}".format(ssid, tid, nid, onid)
|
||||
picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(srv_type, ssid, tid, nid, onid)
|
||||
s_id = "1:0:{:X}:{}:{}:{}:{}:0:0:0:".format(srv_type, ssid, tid, nid, onid)
|
||||
|
||||
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(int(flags[0][2:])) else None
|
||||
locked = LOCKED_ICON if s_id in blacklist else None
|
||||
|
||||
package = list(filter(lambda x: x.startswith("p:"), all_flags))
|
||||
package = package[0][2:] if package else ""
|
||||
|
||||
if transponder is not None:
|
||||
tr_type, sp, tr = str(transponder).partition(" ")
|
||||
tr_type = TrType(tr_type)
|
||||
tr = tr.split(_SEP)
|
||||
service_type = SERVICE_TYPE.get(data[4], SERVICE_TYPE["-2"])
|
||||
# removing all non printable symbols!
|
||||
srv_name = "".join(c for c in srv[1] if c.isprintable())
|
||||
pol = None
|
||||
fec = None
|
||||
system = None
|
||||
pos = None
|
||||
|
||||
if tr_type is TrType.Satellite:
|
||||
pol = POLARIZATION.get(tr[2], None)
|
||||
fec = FEC.get(tr[3], None)
|
||||
system = "DVB-S2" if len(tr) > 7 else "DVB-S"
|
||||
pos = "{}.{}".format(tr[4][:-1], tr[4][-1:])
|
||||
if tr_type is TrType.Terrestrial:
|
||||
system = T_SYSTEM.get(tr[9], None)
|
||||
pos = "T"
|
||||
fec = T_FEC.get(tr[3], None)
|
||||
elif tr_type is TrType.Cable:
|
||||
system = "DVB-C"
|
||||
pos = "C"
|
||||
fec = FEC_DEFAULT.get(tr[4])
|
||||
|
||||
services_list.append(Service(flags_cas=srv[2],
|
||||
transponder_type=tr_type.value,
|
||||
coded=coded,
|
||||
service=srv_name,
|
||||
locked=locked,
|
||||
hide=hide,
|
||||
package=package,
|
||||
service_type=service_type,
|
||||
picon=None,
|
||||
picon_id=picon_id,
|
||||
ssid=data[0],
|
||||
freq=tr[0],
|
||||
rate=tr[1],
|
||||
pol=pol,
|
||||
fec=fec,
|
||||
system=system,
|
||||
pos=pos,
|
||||
data_id=data_id,
|
||||
fav_id=fav_id,
|
||||
transponder=transponder))
|
||||
return services_list
|
||||
|
||||
|
||||
def split(itr, size):
|
||||
""" Divide the iterable. """
|
||||
srv = []
|
||||
tmp = []
|
||||
for i, line in enumerate(itr):
|
||||
tmp.append(line)
|
||||
if i % size == 0:
|
||||
srv.append(tuple(tmp))
|
||||
tmp.clear()
|
||||
|
||||
return srv
|
||||
with open(self._path + "lamedb5", "w", encoding="utf-8") as file:
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -3,9 +3,10 @@ import re
|
||||
from enum import Enum
|
||||
from urllib.parse import unquote, quote
|
||||
|
||||
from app.commons import log
|
||||
from app.eparser.ecommons import BqServiceType, Service
|
||||
from app.settings import SettingsType
|
||||
from app.ui.uicommons import IPTV_ICON
|
||||
from .ecommons import BqServiceType, Service
|
||||
|
||||
# url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group
|
||||
NEUTRINO_FAV_ID_FORMAT = "{}::{}::{}::{}::{}::{}::{}::{}::{}::{}"
|
||||
@@ -19,33 +20,77 @@ class StreamType(Enum):
|
||||
NONE_REC_1 = "5001"
|
||||
NONE_REC_2 = "5002"
|
||||
E_SERVICE_URI = "8193"
|
||||
E_SERVICE_HLS = "8739"
|
||||
|
||||
|
||||
def parse_m3u(path, s_type):
|
||||
with open(path) as file:
|
||||
def parse_m3u(path, s_type, detect_encoding=True, params=None):
|
||||
with open(path, "rb") as file:
|
||||
data = file.read()
|
||||
encoding = "utf-8"
|
||||
|
||||
if detect_encoding:
|
||||
try:
|
||||
import chardet
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
else:
|
||||
enc = chardet.detect(data)
|
||||
encoding = enc.get("encoding", "utf-8")
|
||||
|
||||
aggr = [None] * 10
|
||||
s_aggr = aggr[: -3]
|
||||
services = []
|
||||
groups = set()
|
||||
counter = 0
|
||||
marker_counter = 1
|
||||
sid_counter = 1
|
||||
name = None
|
||||
picon = None
|
||||
p_id = "1_0_1_0_0_0_0_0_0_0.png"
|
||||
st = BqServiceType.IPTV.name
|
||||
params = params or [0, 0, 0, 0]
|
||||
|
||||
for line in file.readlines():
|
||||
for line in str(data, encoding=encoding, errors="ignore").splitlines():
|
||||
if line.startswith("#EXTINF"):
|
||||
name = line[1 + line.index(","):].strip()
|
||||
inf, sep, line = line.partition(" ")
|
||||
if not line:
|
||||
line = inf
|
||||
line, sep, name = line.rpartition(",")
|
||||
|
||||
data = re.split('"', line)
|
||||
size = len(data)
|
||||
if size < 3:
|
||||
continue
|
||||
d = {data[i].lower().strip(" ="): data[i + 1] for i in range(0, len(data) - 1, 2)}
|
||||
picon = d.get("tvg-logo", None)
|
||||
|
||||
grp_name = d.get("group-title", None)
|
||||
if grp_name not in groups:
|
||||
groups.add(grp_name)
|
||||
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
|
||||
marker_counter += 1
|
||||
mr = Service(None, None, None, grp_name, None, None, None, BqServiceType.MARKER.name, None, None,
|
||||
None, None, None, None, None, None, None, None, fav_id, None)
|
||||
services.append(mr)
|
||||
elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2:
|
||||
grp_name = line.strip("#EXTGRP:").strip()
|
||||
if grp_name not in groups:
|
||||
groups.add(grp_name)
|
||||
fav_id = MARKER_FORMAT.format(counter, grp_name, grp_name)
|
||||
counter += 1
|
||||
mr = Service(None, None, None, grp_name, *aggr[0:3], BqServiceType.MARKER.name, *aggr, fav_id, None)
|
||||
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
|
||||
marker_counter += 1
|
||||
mr = Service(None, None, None, grp_name, None, None, None, BqServiceType.MARKER.name, None, None,
|
||||
None, None, None, None, None, None, None, None, fav_id, None)
|
||||
services.append(mr)
|
||||
elif not line.startswith("#"):
|
||||
url = line.strip()
|
||||
fav_id = get_fav_id(url, name, s_type)
|
||||
if name and url:
|
||||
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], BqServiceType.IPTV.name, *aggr, fav_id, None)
|
||||
params[0] = sid_counter
|
||||
sid_counter += 1
|
||||
fav_id = get_fav_id(url, name, s_type, params)
|
||||
if all((name, url, fav_id)):
|
||||
srv = Service(None, None, IPTV_ICON, name, None, None, None, st, picon, p_id, None, None, None,
|
||||
None, None, None, None, url, fav_id, None)
|
||||
services.append(srv)
|
||||
else:
|
||||
log("*.m3u* parse error ['{}']: name[{}], url[{}], fav id[{}]".format(path, name, url, fav_id))
|
||||
|
||||
return services
|
||||
|
||||
@@ -73,12 +118,15 @@ def export_to_m3u(path, bouquet, s_type):
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
def get_fav_id(url, service_name, s_type):
|
||||
def get_fav_id(url, service_name, settings_type, params=None, stream_type=None, s_type=1):
|
||||
""" Returns fav id depending on the profile. """
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
stream_type = StreamType.NONE_TS.value
|
||||
return ENIGMA2_FAV_ID_FORMAT.format(stream_type, 1, 0, 0, 0, 0, quote(url), service_name, service_name, None)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
if settings_type is SettingsType.ENIGMA_2:
|
||||
stream_type = stream_type or StreamType.NONE_TS.value
|
||||
params = params or (0, 0, 0, 0)
|
||||
v1, v2, v3, v4 = params
|
||||
return ENIGMA2_FAV_ID_FORMAT.format(stream_type, s_type, v1, v2, v3, v4, quote(url),
|
||||
service_name, service_name, None)
|
||||
elif settings_type is SettingsType.NEUTRINO_MP:
|
||||
return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)
|
||||
|
||||
|
||||
|
||||
@@ -89,11 +89,8 @@ def parse_webtv(path, name, bq_type):
|
||||
group = group.value if group else group
|
||||
fav_id = NEUTRINO_FAV_ID_FORMAT.format(url, description, urlkey, account, usrname, psw, s_type, iconsrc,
|
||||
iconsrc_b, group)
|
||||
srv = BouquetService(name=title,
|
||||
type=BqServiceType.IPTV,
|
||||
data=fav_id,
|
||||
num=0)
|
||||
services.append(srv)
|
||||
services.append(BouquetService(name=title, type=BqServiceType.IPTV, data=fav_id, num=0))
|
||||
|
||||
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None)
|
||||
bouquets[2].append(bouquet)
|
||||
|
||||
@@ -125,14 +122,15 @@ def write_bouquet(file, bouquet):
|
||||
root.appendChild(bq_elem)
|
||||
|
||||
for srv in bq.services:
|
||||
f_data = srv.flags_cas.split(":")
|
||||
tr_id, on, ssid = srv.fav_id.split(":")
|
||||
srv_elem = doc.createElement("S")
|
||||
srv_elem.setAttribute("i", ssid)
|
||||
srv_elem.setAttribute("n", srv.service)
|
||||
srv_elem.setAttribute("t", tr_id)
|
||||
srv_elem.setAttribute("on", on)
|
||||
srv_elem.setAttribute("s", srv.pos.replace(".", ""))
|
||||
srv_elem.setAttribute("frq", srv.freq[:-3])
|
||||
srv_elem.setAttribute("s", f_data[1])
|
||||
srv_elem.setAttribute("frq", srv.freq)
|
||||
srv_elem.setAttribute("l", "0") # temporary !!!
|
||||
bq_elem.appendChild(srv_elem)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from xml.dom.minidom import parse, Document
|
||||
|
||||
from app.commons import log
|
||||
from ..ecommons import Service, POLARIZATION, FEC, SYSTEM, SERVICE_TYPE, PROVIDER
|
||||
|
||||
_FILE = "services.xml"
|
||||
@@ -28,7 +29,7 @@ def write_services(path, services):
|
||||
tr_atr = sat.split(":")
|
||||
sat_elem = doc.createElement("sat")
|
||||
sat_elem.setAttribute("name", tr_atr[0])
|
||||
sat_elem.setAttribute("position", tr_atr[1].replace(".", ""))
|
||||
sat_elem.setAttribute("position", tr_atr[1])
|
||||
sat_elem.setAttribute("diseqc", tr_atr[2])
|
||||
sat_elem.setAttribute("uncommited", tr_atr[3])
|
||||
root.appendChild(sat_elem)
|
||||
@@ -88,7 +89,6 @@ def parse_services(path):
|
||||
if elem.hasAttributes():
|
||||
sat_name = elem.attributes["name"].value
|
||||
sat_pos = elem.attributes["position"].value
|
||||
sat_pos = "{}.{}".format(sat_pos[:-1], sat_pos[-1:])
|
||||
diseqc = elem.attributes.get("diseqc")
|
||||
diseqc = diseqc.value if diseqc else diseqc
|
||||
uncommited = elem.attributes.get("uncommited")
|
||||
@@ -117,6 +117,15 @@ def parse_transponder(api, sat, sat_pos, services, tr_elem):
|
||||
|
||||
tr = "{}:{}:{}:{}:{}:{}:{}:{}:{}".format(tr_id, on, freq, inv, rate, fec, pol, mod, sys)
|
||||
tr_id = tr_id.lstrip("0")
|
||||
pol = POLARIZATION.get(pol)
|
||||
# Formatting displayed values.
|
||||
try:
|
||||
freq = "{}".format(int(freq) // 1000)
|
||||
rate = "{}".format(int(rate) // 1000)
|
||||
sat_pos = int(sat_pos)
|
||||
sat_pos = "{:0.1f}{}".format(abs(sat_pos / 10), "W" if sat_pos < 0 else "E")
|
||||
except ValueError as e:
|
||||
log("Neutrino parsing error [parse_transponder]: {}".format(e))
|
||||
|
||||
for srv_elem in tr_elem.getElementsByTagName("S"):
|
||||
if srv_elem.hasAttributes():
|
||||
@@ -141,27 +150,10 @@ def parse_transponder(api, sat, sat_pos, services, tr_elem):
|
||||
data_id = "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}".format(api, srv_type, sys, num, f, v, a, p, pmt, tx, vt)
|
||||
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
|
||||
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
|
||||
prv, st, = PROVIDER.get(int(on, 16)), SERVICE_TYPE.get(str(int(srv_type, 16)), SERVICE_TYPE.get("-2"))
|
||||
|
||||
srv = Service(flags_cas=sat,
|
||||
transponder_type=None,
|
||||
coded=None,
|
||||
service=name,
|
||||
locked=None,
|
||||
hide=None,
|
||||
package=PROVIDER.get(int(on, 16)),
|
||||
service_type=SERVICE_TYPE.get(str(int(srv_type, 16))),
|
||||
picon=None,
|
||||
picon_id=picon_id,
|
||||
ssid=ssid,
|
||||
freq=freq,
|
||||
rate=rate,
|
||||
pol=POLARIZATION.get(pol),
|
||||
fec=FEC.get(fec),
|
||||
system=SYSTEM.get(sys),
|
||||
pos=sat_pos,
|
||||
data_id=data_id,
|
||||
fav_id=fav_id,
|
||||
transponder=tr)
|
||||
srv = Service(sat, None, None, name, None, None, prv, st, None, picon_id, ssid, freq, rate, pol,
|
||||
FEC.get(fec), SYSTEM.get(sys), sat_pos, data_id, fav_id, tr)
|
||||
services.append(srv)
|
||||
|
||||
|
||||
|
||||
@@ -53,9 +53,9 @@ def write_satellites(satellites, data_path):
|
||||
transponder_child.setAttribute("frequency", tr.frequency)
|
||||
transponder_child.setAttribute("symbol_rate", tr.symbol_rate)
|
||||
transponder_child.setAttribute("polarization", get_key_by_value(POLARIZATION, tr.polarization))
|
||||
transponder_child.setAttribute("fec_inner", get_key_by_value(FEC, tr.fec_inner))
|
||||
transponder_child.setAttribute("system", get_key_by_value(SYSTEM, tr.system))
|
||||
transponder_child.setAttribute("modulation", get_key_by_value(MODULATION, tr.modulation))
|
||||
transponder_child.setAttribute("fec_inner", get_key_by_value(FEC, tr.fec_inner) or "0")
|
||||
transponder_child.setAttribute("system", get_key_by_value(SYSTEM, tr.system) or "0")
|
||||
transponder_child.setAttribute("modulation", get_key_by_value(MODULATION, tr.modulation) or "0")
|
||||
if tr.pls_mode:
|
||||
transponder_child.setAttribute("pls_mode", tr.pls_mode)
|
||||
if tr.pls_code:
|
||||
@@ -90,7 +90,6 @@ def parse_transponders(elem, sat_name):
|
||||
atr["is_id"].value if "is_id" in atr else None)
|
||||
except Exception as e:
|
||||
message = "Error: can't parse transponder for '{}' satellite! {}".format(sat_name, repr(e))
|
||||
print(message)
|
||||
log(message)
|
||||
else:
|
||||
transponders.append(tr)
|
||||
|
||||
198
app/settings.py
198
app/settings.py
@@ -5,16 +5,17 @@ import os
|
||||
import sys
|
||||
from enum import Enum, IntEnum
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from textwrap import dedent
|
||||
|
||||
HOME_PATH = str(Path.home())
|
||||
CONFIG_PATH = HOME_PATH + "/.config/demon-editor/"
|
||||
SEP = os.sep
|
||||
HOME_PATH = os.path.expanduser("~")
|
||||
CONFIG_PATH = HOME_PATH + "{}.config{}demon-editor{}".format(SEP, SEP, SEP)
|
||||
CONFIG_FILE = CONFIG_PATH + "config.json"
|
||||
DATA_PATH = HOME_PATH + "/DemonEditor/data/"
|
||||
DATA_PATH = HOME_PATH + "{}DemonEditor{}data{}".format(SEP, SEP, SEP)
|
||||
|
||||
IS_DARWIN = sys.platform == "darwin"
|
||||
IS_WIN = sys.platform == "win32"
|
||||
|
||||
|
||||
class Defaults(Enum):
|
||||
@@ -30,19 +31,22 @@ class Defaults(Enum):
|
||||
USE_COLORS = True
|
||||
NEW_COLOR = "rgb(255,230,204)"
|
||||
EXTRA_COLOR = "rgb(179,230,204)"
|
||||
TOOLTIP_LOGO_SIZE = 96
|
||||
LIST_PICON_SIZE = 32
|
||||
FAV_CLICK_MODE = 0
|
||||
PLAY_STREAMS_MODE = 1 if IS_DARWIN else 0
|
||||
STREAM_LIB = "gst" if IS_WIN else "vlc"
|
||||
PROFILE_FOLDER_DEFAULT = False
|
||||
RECORDS_PATH = DATA_PATH + "records/"
|
||||
RECORDS_PATH = DATA_PATH + "records{}".format(SEP)
|
||||
ACTIVATE_TRANSCODING = False
|
||||
ACTIVE_TRANSCODING_PRESET = "720p TV/device"
|
||||
ACTIVE_TRANSCODING_PRESET = "720p TV{}device".format(SEP)
|
||||
|
||||
|
||||
def get_settings():
|
||||
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
|
||||
write_settings(get_default_settings())
|
||||
|
||||
with open(CONFIG_FILE, "r") as config_file:
|
||||
with open(CONFIG_FILE, "r", encoding="utf-8") as config_file:
|
||||
return json.load(config_file)
|
||||
|
||||
|
||||
@@ -76,18 +80,18 @@ def get_default_transcoding_presets():
|
||||
|
||||
def write_settings(config):
|
||||
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
||||
with open(CONFIG_FILE, "w") as config_file:
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as config_file:
|
||||
json.dump(config, config_file, indent=" ")
|
||||
|
||||
|
||||
def set_local_paths(settings, profile_name, data_path=DATA_PATH, use_profile_folder=False):
|
||||
settings["data_local_path"] = "{}{}/".format(data_path, profile_name)
|
||||
settings["data_local_path"] = "{}{}{}".format(data_path, profile_name, SEP)
|
||||
if use_profile_folder:
|
||||
settings["picons_local_path"] = "{}{}/{}/".format(data_path, profile_name, "picons")
|
||||
settings["backup_local_path"] = "{}{}/{}/".format(data_path, profile_name, "backup")
|
||||
settings["picons_local_path"] = "{}{}{}{}{}".format(data_path, profile_name, SEP, "picons", SEP)
|
||||
settings["backup_local_path"] = "{}{}{}{}{}".format(data_path, profile_name, SEP, "backup", SEP)
|
||||
else:
|
||||
settings["picons_local_path"] = "{}{}/{}/".format(data_path, "picons", profile_name)
|
||||
settings["backup_local_path"] = "{}{}/{}/".format(data_path, "backup", profile_name)
|
||||
settings["picons_local_path"] = "{}{}{}{}{}".format(data_path, "picons", SEP, profile_name, SEP)
|
||||
settings["backup_local_path"] = "{}{}{}{}{}".format(data_path, "backup", SEP, profile_name, SEP)
|
||||
|
||||
|
||||
class SettingsType(IntEnum):
|
||||
@@ -97,27 +101,29 @@ class SettingsType(IntEnum):
|
||||
|
||||
def get_default_settings(self):
|
||||
""" Returns default settings for current type """
|
||||
if self is self.ENIGMA_2:
|
||||
if self is SettingsType.ENIGMA_2:
|
||||
return {"setting_type": self.value,
|
||||
"host": "127.0.0.1", "port": "21", "user": "root", "password": "root", "timeout": 5,
|
||||
"http_user": "root", "http_password": "", "http_port": "80",
|
||||
"http_timeout": 5, "http_use_ssl": False,
|
||||
"telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 5,
|
||||
"host": "127.0.0.1", "port": "21", "timeout": 5,
|
||||
"user": "root", "password": "root",
|
||||
"http_port": "80", "http_timeout": 5, "http_use_ssl": False,
|
||||
"telnet_port": "23", "telnet_timeout": 5,
|
||||
"services_path": "/etc/enigma2/", "user_bouquet_path": "/etc/enigma2/",
|
||||
"satellites_xml_path": "/etc/tuxbox/", "data_local_path": DATA_PATH + "enigma2/",
|
||||
"satellites_xml_path": "/etc/tuxbox/", "data_local_path": "{}enigma2{}".format(DATA_PATH, SEP),
|
||||
"picons_path": "/usr/share/enigma2/picon/",
|
||||
"picons_local_path": DATA_PATH + "enigma2/picons/",
|
||||
"backup_local_path": DATA_PATH + "enigma2/backup/"}
|
||||
elif self is self.NEUTRINO_MP:
|
||||
"picons_local_path": "{}enigma2{}picons{}".format(DATA_PATH, SEP, SEP),
|
||||
"backup_local_path": "{}enigma2{}backup{}".format(DATA_PATH, SEP, SEP)}
|
||||
elif self is SettingsType.NEUTRINO_MP:
|
||||
return {"setting_type": self,
|
||||
"host": "127.0.0.1", "port": "21", "user": "root", "password": "root", "timeout": 5,
|
||||
"http_user": "", "http_password": "", "http_port": "80", "http_timeout": 2, "http_use_ssl": False,
|
||||
"telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 1,
|
||||
"host": "127.0.0.1", "port": "21", "timeout": 5,
|
||||
"user": "root", "password": "root",
|
||||
"http_port": "80", "http_timeout": 2, "http_use_ssl": False,
|
||||
"telnet_port": "23", "telnet_timeout": 1,
|
||||
"services_path": "/var/tuxbox/config/zapit/", "user_bouquet_path": "/var/tuxbox/config/zapit/",
|
||||
"satellites_xml_path": "/var/tuxbox/config/", "data_local_path": DATA_PATH + "neutrino/",
|
||||
"satellites_xml_path": "/var/tuxbox/config/",
|
||||
"data_local_path": "{}neutrino{}".format(DATA_PATH, SEP),
|
||||
"picons_path": "/usr/share/tuxbox/neutrino/icons/logo/",
|
||||
"picons_local_path": DATA_PATH + "neutrino/picons/",
|
||||
"backup_local_path": DATA_PATH + "neutrino/backup/"}
|
||||
"picons_local_path": "{}neutrino{}picons{}".format(DATA_PATH, SEP, SEP),
|
||||
"backup_local_path": "{}neutrino{}backup{}".format(DATA_PATH, SEP, SEP)}
|
||||
|
||||
|
||||
class SettingsException(Exception):
|
||||
@@ -131,7 +137,7 @@ class SettingsReadException(SettingsException):
|
||||
class PlayStreamsMode(IntEnum):
|
||||
""" Behavior mode when opening streams. """
|
||||
BUILT_IN = 0
|
||||
VLC = 1
|
||||
WINDOW = 1
|
||||
M3U = 2
|
||||
|
||||
|
||||
@@ -179,7 +185,7 @@ class Settings:
|
||||
self._cp_settings[k] = v
|
||||
|
||||
def_path = self.default_data_path
|
||||
def_path += "enigma2/" if self.setting_type is SettingsType.ENIGMA_2 else "neutrino/"
|
||||
def_path += "enigma2{}".format(SEP) if self.setting_type is SettingsType.ENIGMA_2 else "neutrino{}".format(SEP)
|
||||
set_local_paths(self._cp_settings, self._current_profile, def_path, self.profile_folder_is_default)
|
||||
|
||||
if force_write:
|
||||
@@ -279,22 +285,6 @@ class Settings:
|
||||
def password(self, value):
|
||||
self._cp_settings["password"] = value
|
||||
|
||||
@property
|
||||
def http_user(self):
|
||||
return self._cp_settings.get("http_user", self.get_default("http_user"))
|
||||
|
||||
@http_user.setter
|
||||
def http_user(self, value):
|
||||
self._cp_settings["http_user"] = value
|
||||
|
||||
@property
|
||||
def http_password(self):
|
||||
return self._cp_settings.get("http_password", self.get_default("http_password"))
|
||||
|
||||
@http_password.setter
|
||||
def http_password(self, value):
|
||||
self._cp_settings["http_password"] = value
|
||||
|
||||
@property
|
||||
def http_port(self):
|
||||
return self._cp_settings.get("http_port", self.get_default("http_port"))
|
||||
@@ -319,22 +309,6 @@ class Settings:
|
||||
def http_use_ssl(self, value):
|
||||
self._cp_settings["http_use_ssl"] = value
|
||||
|
||||
@property
|
||||
def telnet_user(self):
|
||||
return self._cp_settings.get("telnet_user", self.get_default("telnet_user"))
|
||||
|
||||
@telnet_user.setter
|
||||
def telnet_user(self, value):
|
||||
self._cp_settings["telnet_user"] = value
|
||||
|
||||
@property
|
||||
def telnet_password(self):
|
||||
return self._cp_settings.get("telnet_password", self.get_default("telnet_password"))
|
||||
|
||||
@telnet_password.setter
|
||||
def telnet_password(self, value):
|
||||
self._cp_settings["telnet_password"] = value
|
||||
|
||||
@property
|
||||
def telnet_port(self):
|
||||
return self._cp_settings.get("telnet_port", self.get_default("telnet_port"))
|
||||
@@ -467,6 +441,14 @@ class Settings:
|
||||
def play_streams_mode(self, value):
|
||||
self._settings["play_streams_mode"] = value
|
||||
|
||||
@property
|
||||
def stream_lib(self):
|
||||
return self._settings.get("stream_lib", Defaults.STREAM_LIB.value)
|
||||
|
||||
@stream_lib.setter
|
||||
def stream_lib(self, value):
|
||||
self._settings["stream_lib"] = value
|
||||
|
||||
# *********** EPG ************ #
|
||||
|
||||
@property
|
||||
@@ -544,30 +526,6 @@ class Settings:
|
||||
def enable_send_to(self, value):
|
||||
self._settings["enable_send_to"] = value
|
||||
|
||||
@property
|
||||
def use_colors(self):
|
||||
return self._settings.get("use_colors", Defaults.USE_COLORS.value)
|
||||
|
||||
@use_colors.setter
|
||||
def use_colors(self, value):
|
||||
self._settings["use_colors"] = value
|
||||
|
||||
@property
|
||||
def new_color(self):
|
||||
return self._settings.get("new_color", Defaults.NEW_COLOR.value)
|
||||
|
||||
@new_color.setter
|
||||
def new_color(self, value):
|
||||
self._settings["new_color"] = value
|
||||
|
||||
@property
|
||||
def extra_color(self):
|
||||
return self._settings.get("extra_color", Defaults.EXTRA_COLOR.value)
|
||||
|
||||
@extra_color.setter
|
||||
def extra_color(self, value):
|
||||
self._settings["extra_color"] = value
|
||||
|
||||
@property
|
||||
def fav_click_mode(self):
|
||||
return self._settings.get("fav_click_mode", Defaults.FAV_CLICK_MODE.value)
|
||||
@@ -612,6 +570,54 @@ class Settings:
|
||||
|
||||
# *********** Appearance *********** #
|
||||
|
||||
@property
|
||||
def list_font(self):
|
||||
return self._settings.get("list_font", "")
|
||||
|
||||
@list_font.setter
|
||||
def list_font(self, value):
|
||||
self._settings["list_font"] = value
|
||||
|
||||
@property
|
||||
def list_picon_size(self):
|
||||
return self._settings.get("list_picon_size", Defaults.LIST_PICON_SIZE.value)
|
||||
|
||||
@list_picon_size.setter
|
||||
def list_picon_size(self, value):
|
||||
self._settings["list_picon_size"] = value
|
||||
|
||||
@property
|
||||
def tooltip_logo_size(self):
|
||||
return self._settings.get("tooltip_logo_size", Defaults.TOOLTIP_LOGO_SIZE.value)
|
||||
|
||||
@tooltip_logo_size.setter
|
||||
def tooltip_logo_size(self, value):
|
||||
self._settings["tooltip_logo_size"] = value
|
||||
|
||||
@property
|
||||
def use_colors(self):
|
||||
return self._settings.get("use_colors", Defaults.USE_COLORS.value)
|
||||
|
||||
@use_colors.setter
|
||||
def use_colors(self, value):
|
||||
self._settings["use_colors"] = value
|
||||
|
||||
@property
|
||||
def new_color(self):
|
||||
return self._settings.get("new_color", Defaults.NEW_COLOR.value)
|
||||
|
||||
@new_color.setter
|
||||
def new_color(self, value):
|
||||
self._settings["new_color"] = value
|
||||
|
||||
@property
|
||||
def extra_color(self):
|
||||
return self._settings.get("extra_color", Defaults.EXTRA_COLOR.value)
|
||||
|
||||
@extra_color.setter
|
||||
def extra_color(self, value):
|
||||
self._settings["extra_color"] = value
|
||||
|
||||
@property
|
||||
def dark_mode(self):
|
||||
return self._settings.get("dark_mode", False)
|
||||
@@ -620,6 +626,22 @@ class Settings:
|
||||
def dark_mode(self, value):
|
||||
self._settings["dark_mode"] = value
|
||||
|
||||
@property
|
||||
def alternate_layout(self):
|
||||
return self._settings.get("alternate_layout", IS_DARWIN)
|
||||
|
||||
@alternate_layout.setter
|
||||
def alternate_layout(self, value):
|
||||
self._settings["alternate_layout"] = value
|
||||
|
||||
@property
|
||||
def bq_details_first(self):
|
||||
return self._settings.get("bq_details_first", False)
|
||||
|
||||
@bq_details_first.setter
|
||||
def bq_details_first(self, value):
|
||||
self._settings["bq_details_first"] = value
|
||||
|
||||
@property
|
||||
def is_themes_support(self):
|
||||
return self._settings.get("is_themes_support", False)
|
||||
@@ -639,7 +661,7 @@ class Settings:
|
||||
@property
|
||||
@lru_cache(1)
|
||||
def themes_path(self):
|
||||
return "{}/.themes/".format(HOME_PATH)
|
||||
return "{}{}.themes{}".format(HOME_PATH, SEP, SEP)
|
||||
|
||||
@property
|
||||
def icon_theme(self):
|
||||
@@ -652,7 +674,7 @@ class Settings:
|
||||
@property
|
||||
@lru_cache(1)
|
||||
def icon_themes_path(self):
|
||||
return "{}/.icons/".format(HOME_PATH)
|
||||
return "{}{}.icons{}".format(HOME_PATH, SEP, SEP)
|
||||
|
||||
@property
|
||||
def is_darwin(self):
|
||||
|
||||
@@ -1,59 +1,359 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from urllib.request import urlopen
|
||||
|
||||
from app.commons import run_task, log, _DATE_FORMAT
|
||||
from app.settings import PlayStreamsMode
|
||||
|
||||
|
||||
class Player:
|
||||
class Player(ABC):
|
||||
""" Base player class. Also used as a factory. """
|
||||
|
||||
@abstractmethod
|
||||
def get_play_mode(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def play(self, mrl=None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def pause(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_time(self, time):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def release(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_playing(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_instance(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
pass
|
||||
|
||||
def get_window_handle(self, widget):
|
||||
""" Returns the identifier [pointer] for the window.
|
||||
|
||||
Based on gtkvlc.py[get_window_pointer] example from here:
|
||||
https://github.com/oaubert/python-vlc/tree/master/examples
|
||||
"""
|
||||
if sys.platform == "linux":
|
||||
return widget.get_window().get_xid()
|
||||
else:
|
||||
is_darwin = sys.platform == "darwin"
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
libgdk = ctypes.CDLL("libgdk-3.0.dylib" if is_darwin else "libgdk-3-0.dll")
|
||||
except OSError as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
else:
|
||||
# https://gitlab.gnome.org/GNOME/pygobject/-/issues/112
|
||||
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
|
||||
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
|
||||
gpointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.get_window().__gpointer__, None)
|
||||
get_pointer = libgdk.gdk_quartz_window_get_nsview if is_darwin else libgdk.gdk_win32_window_get_handle
|
||||
get_pointer.restype = ctypes.c_void_p
|
||||
get_pointer.argtypes = [ctypes.c_void_p]
|
||||
|
||||
return get_pointer(gpointer)
|
||||
|
||||
def get_video_widget(self, widget):
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
area = Gtk.DrawingArea(visible=True)
|
||||
area.connect("draw", self.on_drawing_area_draw)
|
||||
area.set_events(Gdk.ModifierType.BUTTON1_MASK)
|
||||
widget.add(area)
|
||||
|
||||
return area
|
||||
|
||||
def on_drawing_area_draw(self, widget, cr):
|
||||
""" Used for black background drawing in the player drawing area. """
|
||||
cr.set_source_rgb(0, 0, 0)
|
||||
cr.paint()
|
||||
|
||||
@staticmethod
|
||||
def make(name, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
|
||||
""" Factory method. We will not use a separate factory to return a specific implementation.
|
||||
|
||||
@param name: implementation name.
|
||||
@param mode: current player mode [Built-in or windowed].
|
||||
@param widget: parent of video widget.
|
||||
@param buf_cb: buffering callback.
|
||||
@param position_cb: time (position) callback.
|
||||
@param error_cb: error callback.
|
||||
@param playing_cb: playing state callback.
|
||||
|
||||
Throws a NameError if there is no implementation for the given name.
|
||||
"""
|
||||
if name == "mpv":
|
||||
return MpvPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
elif name == "gst":
|
||||
return GstPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
elif name == "vlc":
|
||||
return VlcPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
else:
|
||||
raise NameError("There is no such [{}] implementation.".format(name))
|
||||
|
||||
|
||||
class MpvPlayer(Player):
|
||||
""" Simple wrapper for MPV media player.
|
||||
|
||||
Uses python-mvp [https://github.com/jaseg/python-mpv].
|
||||
"""
|
||||
__INSTANCE = None
|
||||
|
||||
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
try:
|
||||
from app.tools import mpv
|
||||
|
||||
self._player = mpv.MPV(wid=str(self.get_window_handle(self.get_video_widget(widget))))
|
||||
except OSError as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
raise ImportError("No libmpv is found. Check that it is installed!")
|
||||
else:
|
||||
self._mode = mode
|
||||
self._is_playing = False
|
||||
|
||||
@self._player.event_callback(mpv.MpvEventID.FILE_LOADED)
|
||||
def on_open(event):
|
||||
log("Starting playback...")
|
||||
playing_cb()
|
||||
|
||||
@self._player.event_callback(mpv.MpvEventID.END_FILE)
|
||||
def on_end(event):
|
||||
event = event.get("event", {})
|
||||
if event.get("reason", mpv.MpvEventEndFile.ERROR) == mpv.MpvEventEndFile.ERROR:
|
||||
log("Stream playback error: {}".format(event.get("error", mpv.ErrorCode.GENERIC)))
|
||||
error_cb()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
if not cls.__INSTANCE:
|
||||
cls.__INSTANCE = MpvPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
return cls.__INSTANCE
|
||||
|
||||
def get_play_mode(self):
|
||||
return self._mode
|
||||
|
||||
@run_task
|
||||
def play(self, mrl=None):
|
||||
if not mrl:
|
||||
return
|
||||
|
||||
self._player.play(mrl)
|
||||
self._is_playing = True
|
||||
|
||||
@run_task
|
||||
def stop(self):
|
||||
self._player.stop()
|
||||
self._is_playing = True
|
||||
|
||||
def pause(self):
|
||||
pass
|
||||
|
||||
def set_time(self, time):
|
||||
pass
|
||||
|
||||
@run_task
|
||||
def release(self):
|
||||
self._player.terminate()
|
||||
self.__INSTANCE = None
|
||||
|
||||
def is_playing(self):
|
||||
return self._is_playing
|
||||
|
||||
|
||||
class GstPlayer(Player):
|
||||
""" Simple wrapper for GStreamer playbin. """
|
||||
|
||||
__INSTANCE = None
|
||||
|
||||
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
try:
|
||||
import gi
|
||||
|
||||
gi.require_version("Gst", "1.0")
|
||||
gi.require_version("GstVideo", "1.0")
|
||||
from gi.repository import Gst, GstVideo
|
||||
# Initialization of GStreamer.
|
||||
Gst.init(sys.argv)
|
||||
except (OSError, ValueError) as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
raise ImportError("No GStreamer is found. Check that it is installed!")
|
||||
else:
|
||||
self._error_cb = error_cb
|
||||
self._playing_cb = playing_cb
|
||||
|
||||
self.STATE = Gst.State
|
||||
self.STAT_RETURN = Gst.StateChangeReturn
|
||||
|
||||
self._mode = mode
|
||||
self._is_playing = False
|
||||
self._player = Gst.ElementFactory.make("playbin", "player")
|
||||
# Initialization of the playback widget.
|
||||
vid_widget = self.get_video_widget(widget)
|
||||
widget.add(vid_widget)
|
||||
vid_widget.show()
|
||||
self._player.set_window_handle(self.get_window_handle(vid_widget))
|
||||
|
||||
bus = self._player.get_bus()
|
||||
bus.add_signal_watch()
|
||||
bus.connect("message::error", self.on_error)
|
||||
bus.connect("message::state-changed", self.on_state_changed)
|
||||
bus.connect("message::eos", self.on_eos)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
|
||||
if not cls.__INSTANCE:
|
||||
cls.__INSTANCE = GstPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
return cls.__INSTANCE
|
||||
|
||||
def get_play_mode(self):
|
||||
return self._mode
|
||||
|
||||
def play(self, mrl=None):
|
||||
self._player.set_state(self.STATE.READY)
|
||||
if not mrl:
|
||||
return
|
||||
|
||||
self._player.set_property("uri", mrl)
|
||||
|
||||
log("Setting the URL for playback: {}".format(mrl))
|
||||
ret = self._player.set_state(self.STATE.PLAYING)
|
||||
|
||||
if ret == self.STAT_RETURN.FAILURE:
|
||||
log("ERROR: Unable to set the 'PLAYING' state for '{}'.".format(mrl))
|
||||
else:
|
||||
self._is_playing = True
|
||||
|
||||
def stop(self):
|
||||
log("Stop playback...")
|
||||
self._player.set_state(self.STATE.READY)
|
||||
self._is_playing = False
|
||||
|
||||
def pause(self):
|
||||
self._player.set_state(self.STATE.PAUSED)
|
||||
|
||||
def set_time(self, time):
|
||||
pass
|
||||
|
||||
@run_task
|
||||
def release(self):
|
||||
self._is_playing = False
|
||||
self._player.set_state(self.STATE.NULL)
|
||||
self.__INSTANCE = None
|
||||
|
||||
def set_mrl(self, mrl):
|
||||
self._player.set_property("uri", mrl)
|
||||
|
||||
def is_playing(self):
|
||||
return self._is_playing
|
||||
|
||||
def on_error(self, bus, msg):
|
||||
err, dbg = msg.parse_error()
|
||||
log(err)
|
||||
self._error_cb()
|
||||
|
||||
def on_state_changed(self, bus, msg):
|
||||
if not msg.src == self._player:
|
||||
# Not from the player.
|
||||
return
|
||||
|
||||
old_state, new_state, pending = msg.parse_state_changed()
|
||||
if new_state is self.STATE.PLAYING:
|
||||
log("Starting playback...")
|
||||
self._playing_cb()
|
||||
self.get_stream_info()
|
||||
|
||||
def on_eos(self, bus, msg):
|
||||
""" Called when an end-of-stream message appears. """
|
||||
self._player.set_state(self.STATE.READY)
|
||||
self._is_playing = False
|
||||
|
||||
def get_stream_info(self):
|
||||
log("Getting stream info...")
|
||||
nr_video = self._player.get_property("n-video")
|
||||
for i in range(nr_video):
|
||||
# Retrieve the stream's video tags.
|
||||
tags = self._player.emit("get-video-tags", i)
|
||||
if tags:
|
||||
_, cod = tags.get_string("video-codec")
|
||||
log("Video codec: {}".format(cod or "unknown"))
|
||||
|
||||
nr_audio = self._player.get_property("n-audio")
|
||||
for i in range(nr_audio):
|
||||
# Retrieve the stream's video tags.
|
||||
tags = self._player.emit("get-audio-tags", i)
|
||||
if tags:
|
||||
_, cod = tags.get_string("audio-codec")
|
||||
log("Audio codec: {}".format(cod or "unknown"))
|
||||
|
||||
|
||||
class VlcPlayer(Player):
|
||||
""" Simple wrapper for VLC media player.
|
||||
|
||||
Uses python-vlc [https://github.com/oaubert/python-vlc].
|
||||
"""
|
||||
|
||||
__VLC_INSTANCE = None
|
||||
__PLAY_STREAMS_MODE = PlayStreamsMode.BUILT_IN
|
||||
|
||||
def __init__(self, rewind_callback, position_callback, error_callback, playing_callback):
|
||||
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
|
||||
try:
|
||||
from app.tools import vlc
|
||||
from app.tools.vlc import EventType
|
||||
except OSError as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
raise ImportError
|
||||
else:
|
||||
self._is_playing = False
|
||||
|
||||
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
|
||||
self._player = vlc.Instance(args).media_player_new()
|
||||
except (OSError, AttributeError) as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
raise ImportError("No VLC is found. Check that it is installed!")
|
||||
else:
|
||||
self._mode = mode
|
||||
self._is_playing = False
|
||||
|
||||
ev_mgr = self._player.event_manager()
|
||||
|
||||
if rewind_callback:
|
||||
if buf_cb:
|
||||
# TODO look other EventType options
|
||||
ev_mgr.event_attach(EventType.MediaPlayerBuffering,
|
||||
lambda et, p: rewind_callback(p.get_media().get_duration()),
|
||||
lambda et, p: buf_cb(p.get_media().get_duration()),
|
||||
self._player)
|
||||
if position_callback:
|
||||
if position_cb:
|
||||
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
|
||||
lambda et, p: position_callback(p.get_time()),
|
||||
lambda et, p: position_cb(p.get_time()),
|
||||
self._player)
|
||||
|
||||
if error_callback:
|
||||
if error_cb:
|
||||
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError,
|
||||
lambda et, p: error_callback(),
|
||||
lambda et, p: error_cb(),
|
||||
self._player)
|
||||
if playing_callback:
|
||||
if playing_cb:
|
||||
ev_mgr.event_attach(EventType.MediaPlayerPlaying,
|
||||
lambda et, p: playing_callback(),
|
||||
lambda et, p: playing_cb(),
|
||||
self._player)
|
||||
|
||||
self.init_video_widget(widget)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, rewind_callback=None, position_callback=None, error_callback=None, playing_callback=None):
|
||||
def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
|
||||
if not cls.__VLC_INSTANCE:
|
||||
cls.__VLC_INSTANCE = Player(rewind_callback, position_callback, error_callback, playing_callback)
|
||||
cls.__VLC_INSTANCE = VlcPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
|
||||
return cls.__VLC_INSTANCE
|
||||
|
||||
@staticmethod
|
||||
def get_play_mode():
|
||||
return Player.__PLAY_STREAMS_MODE
|
||||
def get_play_mode(self):
|
||||
return self._mode
|
||||
|
||||
@run_task
|
||||
def play(self, mrl=None):
|
||||
@@ -80,29 +380,7 @@ class Player:
|
||||
self._is_playing = False
|
||||
self._player.stop()
|
||||
self._player.release()
|
||||
|
||||
def set_xwindow(self, xid):
|
||||
self._player.set_xwindow(xid)
|
||||
|
||||
def set_nso(self, widget):
|
||||
""" Used on MacOS to set NSObject.
|
||||
|
||||
Based on gtkvlc.py[get_window_pointer] example from here:
|
||||
https://github.com/oaubert/python-vlc/tree/master/examples
|
||||
"""
|
||||
try:
|
||||
import ctypes
|
||||
g_dll = ctypes.CDLL("libgdk-3.0.dylib")
|
||||
except OSError as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
else:
|
||||
get_nsview = g_dll.gdk_quartz_window_get_nsview
|
||||
get_nsview.restype, get_nsview.argtypes = ctypes.c_void_p, [ctypes.c_void_p]
|
||||
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
|
||||
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
|
||||
# Get the C void* pointer to the window
|
||||
pointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.get_window().__gpointer__, None)
|
||||
self._player.set_nsobject(get_nsview(pointer))
|
||||
self.__VLC_INSTANCE = None
|
||||
|
||||
def set_mrl(self, mrl):
|
||||
self._player.set_mrl(mrl)
|
||||
@@ -110,94 +388,14 @@ class Player:
|
||||
def is_playing(self):
|
||||
return self._is_playing
|
||||
|
||||
def set_full_screen(self, full):
|
||||
self._player.set_fullscreen(full)
|
||||
|
||||
|
||||
class HttpPlayer:
|
||||
""" Simple wrapper for VLC media player to interact over http. """
|
||||
|
||||
__VLC_INSTANCE = None
|
||||
__PLAY_STREAMS_MODE = PlayStreamsMode.VLC
|
||||
|
||||
class Commands(Enum):
|
||||
STATUS = "http://127.0.0.1:{}/requests/status.xml"
|
||||
PLAY = "http://127.0.0.1:{}/requests/status.xml?command=in_play&input={}"
|
||||
STOP = "http://127.0.0.1:{}/requests/status.xml?command=pl_stop"
|
||||
CLEAR = "http://127.0.0.1:{}/requests/status.xml?command=pl_empty"
|
||||
|
||||
def __init__(self, exe, port, is_darwin):
|
||||
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
|
||||
|
||||
self._executor = PoolExecutor(max_workers=1)
|
||||
self._cmd = [exe, "--no-stats", "--verbose=-1", "--extraintf", "http", "--http-port", port, "--quiet"]
|
||||
if not is_darwin:
|
||||
self._cmd.append("--one-instance")
|
||||
|
||||
self._p = None
|
||||
self._state = None
|
||||
self._port = port
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, settings):
|
||||
if not cls.__VLC_INSTANCE:
|
||||
import shutil
|
||||
|
||||
is_darwin = settings.is_darwin
|
||||
# TODO Add options[vlc_exe and port] to the settings!
|
||||
exe = "/Applications/VLC.app/Contents/MacOS/VLC" if is_darwin else "/usr/bin/vlc"
|
||||
if shutil.which(exe) is None:
|
||||
raise ImportError
|
||||
cls.__VLC_INSTANCE = HttpPlayer(exe=exe, port=str(9090), is_darwin=is_darwin)
|
||||
return cls.__VLC_INSTANCE
|
||||
|
||||
@staticmethod
|
||||
def get_play_mode():
|
||||
return HttpPlayer.__PLAY_STREAMS_MODE
|
||||
|
||||
@run_task
|
||||
def play(self, mrl=None):
|
||||
if not self._p or self._p and self._p.poll() is not None:
|
||||
self._p = subprocess.Popen(self._cmd + [mrl], preexec_fn=os.setsid)
|
||||
self._p.communicate()
|
||||
def init_video_widget(self, widget):
|
||||
video_widget = self.get_video_widget(widget)
|
||||
if sys.platform == "linux":
|
||||
self._player.set_xwindow(video_widget.get_window().get_xid())
|
||||
elif sys.platform == "darwin":
|
||||
self._player.set_nsobject(self.get_window_handle(video_widget))
|
||||
else:
|
||||
self._executor.submit(self.open_command, self.Commands.CLEAR)
|
||||
self._executor.submit(self.open_command, self.Commands.PLAY, mrl)
|
||||
|
||||
def open_command(self, command, url=None):
|
||||
if command is self.Commands.PLAY:
|
||||
url = self.Commands.PLAY.value.format(self._port, url)
|
||||
else:
|
||||
url = command.value.format(self._port)
|
||||
|
||||
try:
|
||||
with urlopen(url, timeout=5) as f:
|
||||
self._state = command
|
||||
except Exception as e:
|
||||
log("{}[open_command, {}] error: {}".format(__class__.__name__, command, e))
|
||||
|
||||
def stop(self):
|
||||
if self._state is self.Commands.PLAY:
|
||||
self._executor.submit(self.open_command, self.Commands.STOP)
|
||||
|
||||
def pause(self):
|
||||
pass
|
||||
|
||||
def set_time(self, time):
|
||||
pass
|
||||
|
||||
@run_task
|
||||
def release(self):
|
||||
if self._p and self._p.poll() is None:
|
||||
import signal
|
||||
# Good explanation here: https://stackoverflow.com/a/4791612
|
||||
os.killpg(os.getpgid(self._p.pid), signal.SIGTERM)
|
||||
|
||||
def is_playing(self):
|
||||
return self._state is self.Commands.PLAY
|
||||
|
||||
def set_full_screen(self, full):
|
||||
pass
|
||||
log("Video widget initialization error: platform '{}' is not supported. ".format(sys.platform))
|
||||
|
||||
|
||||
class Recorder:
|
||||
|
||||
1941
app/tools/mpv.py
Normal file
1941
app/tools/mpv.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,18 +5,22 @@ import shutil
|
||||
from collections import namedtuple
|
||||
from html.parser import HTMLParser
|
||||
|
||||
import requests
|
||||
|
||||
from app.commons import run_task, log
|
||||
from app.settings import SettingsType
|
||||
from .satellites import _HEADERS
|
||||
|
||||
_ENIGMA2_PICON_KEY = "{:X}:{:X}:{}"
|
||||
_NEUTRINO_PICON_KEY = "{:x}{:04x}{:04x}.png"
|
||||
|
||||
Provider = namedtuple("Provider", ["logo", "name", "pos", "url", "on_id", "ssid", "single", "selected"])
|
||||
Picon = namedtuple("Picon", ["ref", "ssid", "v_pid"])
|
||||
Picon = namedtuple("Picon", ["ref", "ssid"])
|
||||
|
||||
|
||||
class PiconsParser(HTMLParser):
|
||||
""" Parser for package html page. (https://www.lyngsat.com/packages/*provider-name*.html) """
|
||||
_BASE_URL = "https://www.lyngsat.com"
|
||||
|
||||
def __init__(self, entities=False, separator=' ', single=None):
|
||||
|
||||
@@ -58,19 +62,16 @@ class PiconsParser(HTMLParser):
|
||||
row = self._current_row
|
||||
ln = len(row)
|
||||
|
||||
if self._single and ln == 4 and row[0].startswith("../../logo/"):
|
||||
self.picons.append(Picon(row[0].strip("../"), "0", "0"))
|
||||
if self._single and ln == 4 and row[0].startswith("/logo/"):
|
||||
self.picons.append(Picon(row[0].strip(), "0"))
|
||||
else:
|
||||
if 9 < ln < 13:
|
||||
if ln > 8:
|
||||
url = None
|
||||
if row[0].startswith("../logo/"):
|
||||
url = row[0]
|
||||
elif row[1].startswith("../logo/"):
|
||||
url = row[1]
|
||||
if row[2].startswith("/logo/"):
|
||||
url = row[2]
|
||||
|
||||
ssid = row[-4]
|
||||
if url and len(ssid) > 2:
|
||||
self.picons.append(Picon(url, ssid, row[-3]))
|
||||
if url and row[0].isdigit():
|
||||
self.picons.append(Picon(url, row[0]))
|
||||
|
||||
self._current_row = []
|
||||
|
||||
@@ -78,37 +79,47 @@ class PiconsParser(HTMLParser):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def parse(open_path, picons_path, tmp_path, provider, picon_ids, s_type=SettingsType.ENIGMA_2):
|
||||
if not os.path.isfile(open_path):
|
||||
log("PiconsParser error [parse]. No such file or directory: {}".format(open_path))
|
||||
def parse(provider, picons_path, picon_ids, s_type=SettingsType.ENIGMA_2):
|
||||
""" Returns tuple(url, picon file name) list. """
|
||||
req = requests.get(provider.url, timeout=5)
|
||||
if req.status_code == 200:
|
||||
logo_data = req.text
|
||||
else:
|
||||
log("Provider picons downloading error: {} {}".format(provider.url, req.reason))
|
||||
return
|
||||
|
||||
with open(open_path, encoding="utf-8", errors="replace") as f:
|
||||
on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single
|
||||
neg_pos = pos.endswith("W")
|
||||
pos = int("".join(c for c in pos if c.isdigit()))
|
||||
# For negative (West) positions 3600 - numeric position value!!!
|
||||
if neg_pos:
|
||||
pos = 3600 - pos
|
||||
parser = PiconsParser(single=single)
|
||||
parser.reset()
|
||||
parser.feed(f.read())
|
||||
picons = parser.picons
|
||||
if picons:
|
||||
os.makedirs(picons_path, exist_ok=True)
|
||||
for p in picons:
|
||||
try:
|
||||
if single:
|
||||
on_id, freq = on_id.strip().split("::")
|
||||
namespace = "{:X}{:X}".format(int(pos), int(freq))
|
||||
else:
|
||||
namespace = "{:X}0000".format(int(pos))
|
||||
name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, s_type)
|
||||
p_name = picons_path + (name if name else os.path.basename(p.ref))
|
||||
shutil.copyfile(tmp_path + "www.lyngsat.com/" + p.ref.lstrip("."), p_name)
|
||||
except (TypeError, ValueError) as e:
|
||||
msg = "Picons format parse error: {}".format(p) + "\n" + str(e)
|
||||
log(msg)
|
||||
on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single
|
||||
neg_pos = pos.endswith("W")
|
||||
pos = int("".join(c for c in pos if c.isdigit()))
|
||||
# For negative (West) positions 3600 - numeric position value!!!
|
||||
if neg_pos:
|
||||
pos = 3600 - pos
|
||||
|
||||
parser = PiconsParser(single=provider.single)
|
||||
parser.reset()
|
||||
parser.feed(logo_data)
|
||||
picons = parser.picons
|
||||
picons_data = []
|
||||
|
||||
if picons:
|
||||
for p in picons:
|
||||
try:
|
||||
if single:
|
||||
on_id, freq = on_id.strip().split("::")
|
||||
namespace = "{:X}{:X}".format(int(pos), int(freq))
|
||||
else:
|
||||
namespace = "{:X}0000".format(int(pos))
|
||||
|
||||
if single and not ssid.isdigit():
|
||||
ssid = "".join(c for c in ssid if c.isdigit()) or "0"
|
||||
name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, s_type)
|
||||
p_name = picons_path + (name if name else os.path.basename(p.ref))
|
||||
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)
|
||||
log(msg)
|
||||
|
||||
return picons_data
|
||||
|
||||
@staticmethod
|
||||
def format(ssid, on_id, namespace, picon_ids, s_type):
|
||||
@@ -127,7 +138,8 @@ class ProviderParser(HTMLParser):
|
||||
_POSITION_PATTERN = re.compile("at\s\d+\..*(?:E|W)']")
|
||||
_ONID_TID_PATTERN = re.compile("^\d+-\d+.*")
|
||||
_TRANSPONDER_FREQUENCY_PATTERN = re.compile("^\d+ [HVLR]+")
|
||||
_DOMAINS = {"/tvchannels/", "/radiochannels/", "/packages/"}
|
||||
_DOMAINS = {"/tvchannels/", "/radiochannels/", "/packages/", "/logo/"}
|
||||
_BASE_URL = "https://www.lyngsat.com"
|
||||
|
||||
def __init__(self, entities=False, separator=' '):
|
||||
|
||||
@@ -155,7 +167,7 @@ class ProviderParser(HTMLParser):
|
||||
if tag == 'tr':
|
||||
self._is_th = True
|
||||
if tag == "img":
|
||||
if attrs[0][1].startswith("logo/"):
|
||||
if attrs[0][1].startswith("/logo/"):
|
||||
self._current_row.append(attrs[0][1])
|
||||
if tag == "a":
|
||||
url = attrs[0][1]
|
||||
@@ -187,41 +199,47 @@ class ProviderParser(HTMLParser):
|
||||
self._current_row.append(final_cell)
|
||||
self._current_cell = []
|
||||
elif tag == 'tr':
|
||||
r = self._current_row
|
||||
row = self._current_row
|
||||
# Satellite position
|
||||
if not self._positon:
|
||||
pos = re.findall(self._POSITION_PATTERN, str(r))
|
||||
pos = re.findall(self._POSITION_PATTERN, str(row))
|
||||
if pos:
|
||||
self._positon = "".join(c for c in str(pos) if c.isdigit() or c in ".EW")
|
||||
|
||||
len_row = len(r)
|
||||
len_row = len(row)
|
||||
if len_row > 2:
|
||||
m = self._TRANSPONDER_FREQUENCY_PATTERN.match(r[1])
|
||||
m = self._TRANSPONDER_FREQUENCY_PATTERN.match(row[1])
|
||||
if m:
|
||||
self._freq = m.group().split()[0]
|
||||
|
||||
if len_row == 12:
|
||||
if len_row == 14:
|
||||
# Providers
|
||||
name = r[5]
|
||||
name = row[6]
|
||||
self._prv_names.add(name)
|
||||
m = self._ONID_TID_PATTERN.match(str(r[-2]))
|
||||
m = self._ONID_TID_PATTERN.match(str(row[9]))
|
||||
if m:
|
||||
on_id, tid = m.group().split("-")
|
||||
if on_id not in self._ids:
|
||||
r[-2] = on_id
|
||||
row[-2] = on_id
|
||||
self._ids.add(on_id)
|
||||
r[0] = self._positon
|
||||
row[0] = self._positon
|
||||
if name + on_id not in self._prv_names:
|
||||
self._prv_names.add(name + on_id)
|
||||
self.rows.append(Provider(logo=r[2], name=name, pos=self._positon, url=r[6], on_id=on_id,
|
||||
logo_data = None
|
||||
req = requests.get(self._BASE_URL + row[3], timeout=5)
|
||||
if req.status_code == 200:
|
||||
logo_data = req.content
|
||||
else:
|
||||
log("Downloading provider logo error: {}".format(req.reason))
|
||||
self.rows.append(Provider(logo=logo_data, name=name, pos=self._positon, url=row[5], on_id=on_id,
|
||||
ssid=None, single=False, selected=True))
|
||||
elif 6 < len_row < 10:
|
||||
elif 6 < len_row < 14:
|
||||
# Single services
|
||||
name, url, ssid = None, None, None
|
||||
if r[0].startswith("http"):
|
||||
name, url, ssid = r[1], r[0], r[4]
|
||||
elif r[1].startswith("http"):
|
||||
name, url, ssid = r[2], r[1], r[5]
|
||||
if row[0].startswith("http"):
|
||||
name, url, ssid = row[1], row[0], row[0]
|
||||
elif row[1].startswith("http"):
|
||||
name, url, ssid = row[2], row[1], row[0]
|
||||
|
||||
if name and url:
|
||||
on_id = "{}::{}".format(self._on_id if self._on_id else "1", self._freq)
|
||||
@@ -237,14 +255,51 @@ class ProviderParser(HTMLParser):
|
||||
super().reset()
|
||||
|
||||
|
||||
def parse_providers(open_path):
|
||||
def parse_providers(url):
|
||||
""" Returns a list of providers sorted by logo [single channels after providers]. """
|
||||
parser = ProviderParser()
|
||||
parser.reset()
|
||||
|
||||
with open(open_path, encoding="utf-8", errors="replace") as f:
|
||||
parser.feed(f.read())
|
||||
request = requests.get(url=url, headers=_HEADERS)
|
||||
if request.status_code == 200:
|
||||
parser.feed(request.text)
|
||||
else:
|
||||
log("Parse providers error [{}]: {}".format(url, request.reason))
|
||||
|
||||
return parser.rows
|
||||
def srt(p):
|
||||
if p.logo is None:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
providers = parser.rows
|
||||
providers.sort(key=srt)
|
||||
|
||||
return providers
|
||||
|
||||
|
||||
def download_picon(src_url, dest_path, callback):
|
||||
""" Downloads and saves the picon to file. """
|
||||
err_msg = "Picon download error: {} [{}]"
|
||||
timeout = (3, 5) # connect and read timeouts
|
||||
|
||||
if callback:
|
||||
callback("Downloading: {}.\n".format(os.path.basename(dest_path)))
|
||||
|
||||
req = requests.get(src_url, timeout=timeout, stream=True)
|
||||
if req.status_code != 200:
|
||||
err_msg = err_msg.format(src_url, req.reason)
|
||||
log(err_msg)
|
||||
if callback:
|
||||
callback(err_msg + "\n")
|
||||
else:
|
||||
try:
|
||||
with open(dest_path, "wb") as f:
|
||||
for chunk in req:
|
||||
f.write(chunk)
|
||||
except OSError as e:
|
||||
err_msg = "Saving picon [{}] error: {}".format(dest_path, e)
|
||||
log(err_msg)
|
||||
if callback:
|
||||
callback(err_msg + "\n")
|
||||
|
||||
|
||||
@run_task
|
||||
|
||||
@@ -14,13 +14,14 @@ from app.eparser import Satellite, Transponder, is_transponder_valid
|
||||
from app.eparser.ecommons import (PLS_MODE, get_key_by_value, FEC, SYSTEM, POLARIZATION, MODULATION, SERVICE_TYPE,
|
||||
Service, CAS)
|
||||
|
||||
_HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/69.0"}
|
||||
_HEADERS = {"User-Agent": "Mozilla/5.0 (Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0"}
|
||||
|
||||
|
||||
class SatelliteSource(Enum):
|
||||
FLYSAT = ("https://www.flysat.com/satlist.php",)
|
||||
LYNGSAT = ("https://www.lyngsat.com/asia.html", "https://www.lyngsat.com/europe.html",
|
||||
"https://www.lyngsat.com/atlantic.html", "https://www.lyngsat.com/america.html")
|
||||
KINGOFSAT = ("https://en.kingofsat.net/satellites.php",)
|
||||
|
||||
@staticmethod
|
||||
def get_sources(src):
|
||||
@@ -96,7 +97,9 @@ class SatellitesParser(HTMLParser):
|
||||
if tag == "tr":
|
||||
self._is_th = True
|
||||
if tag == "a":
|
||||
self._current_row.append(attrs[0][1])
|
||||
for atr in attrs:
|
||||
if atr[0] == "href":
|
||||
self._current_row.append(atr[1])
|
||||
|
||||
def handle_data(self, data):
|
||||
""" Save content to a cell """
|
||||
@@ -182,6 +185,11 @@ class SatellitesParser(HTMLParser):
|
||||
elif r_len == 5:
|
||||
sats.append((row[2], current_pos, row[3], base_url + row[1], False))
|
||||
return sats
|
||||
elif source is SatelliteSource.KINGOFSAT:
|
||||
def get_sat(r):
|
||||
return r[3], self.parse_position(r[1]), None, r[0], False
|
||||
|
||||
return list(map(get_sat, filter(lambda x: len(x) == 17, self._rows)))
|
||||
|
||||
def get_satellite(self, sat):
|
||||
pos = sat[1]
|
||||
@@ -201,16 +209,29 @@ class SatellitesParser(HTMLParser):
|
||||
def get_transponders(self, sat_url):
|
||||
""" Getting transponders(sorted by frequency). """
|
||||
self._rows.clear()
|
||||
url = "https://www.flysat.com/" + sat_url if self._source is SatelliteSource.FLYSAT else sat_url
|
||||
request = requests.get(url=url, headers=_HEADERS)
|
||||
reason = request.reason
|
||||
trs = []
|
||||
if reason == "OK":
|
||||
self.feed(request.text)
|
||||
if self._source is SatelliteSource.FLYSAT:
|
||||
self.get_transponders_for_fly_sat(trs)
|
||||
elif self._source is SatelliteSource.LYNGSAT:
|
||||
self.get_transponders_for_lyng_sat(trs)
|
||||
|
||||
url = sat_url
|
||||
if self._source is SatelliteSource.FLYSAT:
|
||||
url = "https://www.flysat.com/" + sat_url
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
url = "https://en.kingofsat.net/" + sat_url
|
||||
|
||||
try:
|
||||
request = requests.get(url=url, headers=_HEADERS)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
log("Getting transponders error: {}".format(e))
|
||||
else:
|
||||
if request.status_code == 200:
|
||||
self.feed(request.text)
|
||||
if self._source is SatelliteSource.FLYSAT:
|
||||
self.get_transponders_for_fly_sat(trs)
|
||||
elif self._source is SatelliteSource.LYNGSAT:
|
||||
self.get_transponders_for_lyng_sat(trs)
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
self.get_transponders_for_king_of_sat(trs)
|
||||
else:
|
||||
log("SatellitesParser [get transponders] error: {} {}".format(url, request.reason))
|
||||
|
||||
return sorted(trs, key=lambda x: int(x.frequency))
|
||||
|
||||
@@ -266,42 +287,54 @@ class SatellitesParser(HTMLParser):
|
||||
trs.extend(n_trs)
|
||||
|
||||
def get_transponders_for_lyng_sat(self, trs):
|
||||
""" Parsing transponders for LyngSat """
|
||||
""" Parsing transponders for LyngSat. """
|
||||
frq_pol_pattern = re.compile("(\\d{4,5})\\s+([RLHV]).*")
|
||||
sr_fec_pattern = re.compile("^(\\d{4,5})-(\\d/\\d)(.+PSK)?(.*)?$")
|
||||
sys_pattern = re.compile("(DVB-S[2]?) ?(PLS+ (Root|Gold|Combo)+ (\\d+))* ?(multistream stream (\\d+))?",
|
||||
re.IGNORECASE)
|
||||
sr_fec_pattern = re.compile(r"(DVB-S[2]?)\s+(.+PSK)?.*?(\d+)\s+(\d/\d)\s*(?:T2-MI\s+PLP\s+(\d+))?.*")
|
||||
zeros = "000"
|
||||
pls_modes = {v: k for k, v in PLS_MODE.items()}
|
||||
pls_mode, pls_code, pls_id = None, None, None
|
||||
|
||||
for r in filter(lambda x: len(x) > 8, self._rows):
|
||||
for frq in r[1], r[2], r[3]:
|
||||
for row in filter(lambda x: len(x) > 8, self._rows):
|
||||
for frq in row[1], row[2], row[3]:
|
||||
freq = re.match(frq_pol_pattern, frq)
|
||||
if freq:
|
||||
break
|
||||
if not freq:
|
||||
continue
|
||||
|
||||
frq, pol = freq.group(1), freq.group(2)
|
||||
sr_fec = re.match(sr_fec_pattern, r[-3])
|
||||
srf = " ".join(row[3:5])
|
||||
sr_fec = re.search(sr_fec_pattern, srf)
|
||||
if not sr_fec:
|
||||
continue
|
||||
sr, fec, mod = sr_fec.group(1), sr_fec.group(2), sr_fec.group(3)
|
||||
|
||||
sys, mod, sr, fec = sr_fec.group(1), sr_fec.group(2), sr_fec.group(3), sr_fec.group(4)
|
||||
mod = mod.strip() if mod else "Auto"
|
||||
|
||||
res = re.match(sys_pattern, r[-4])
|
||||
if not res:
|
||||
continue
|
||||
|
||||
sys = res.group(1)
|
||||
pls_mode = res.group(3)
|
||||
pls_mode = pls_modes.get(pls_mode.capitalize(), None) if pls_mode else pls_mode
|
||||
pls_code = res.group(4)
|
||||
pls_id = res.group(6)
|
||||
pls_id = sr_fec.group(5)
|
||||
|
||||
tr = Transponder(frq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, pls_id)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
def get_transponders_for_king_of_sat(self, trs):
|
||||
""" Getting transponders for KingOfSat source.
|
||||
|
||||
Since the *.ini file contains incomplete information, it is not used.
|
||||
"""
|
||||
zeros = "000"
|
||||
pos_pat = re.compile(r".*?(\d+\.\d°[EW]).*")
|
||||
pat = re.compile(
|
||||
r"(\d+).00\s+([RLHV])\s+(DVB-S[2]?)\s+(?:T2-MI, PLP (\d+)\s+)?(.*PSK).*?(?:Stream\s+(\d+))?\s+(\d+)\s+(\d+/\d+)$")
|
||||
|
||||
for row in filter(lambda r: len(r) == 16 and pos_pat.match(r[0]), self._rows):
|
||||
res = pat.search(" ".join((row[0], row[2], row[3], row[8], row[9], row[10])))
|
||||
if res:
|
||||
freq, sr, pol, fec, sys = res.group(1), res.group(7), res.group(2), res.group(8), res.group(3)
|
||||
mod, pls_id, pls_code = res.group(5), res.group(4), res.group(6)
|
||||
|
||||
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, None, pls_code, pls_id)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
|
||||
class ServicesParser(HTMLParser):
|
||||
""" Services parser for LYNGSAT source. """
|
||||
@@ -313,8 +346,9 @@ class ServicesParser(HTMLParser):
|
||||
self._S_TYPES = {"": "2", "MPEG-2 SD": "1", "SD": "1", "MPEG-4 SD": "22", "HEVC SD": "22", "MPEG-4 HD": "25",
|
||||
"MPEG-4 HD 1080": "25", "MPEG-4 HD 720": "25", "HEVC HD": "25", "HEVC UHD": "31",
|
||||
"HEVC UHD 4K": "31"}
|
||||
self._TR_PAT = re.compile(r"(DVB-S[2]?)/?(.*PSK)?\s+SR\s+(\d+)\s+FEC\s+(\d/\d).*ONID/TID:\s+(\d+)/(\d+)\s+.*")
|
||||
self._PTR_PAT = re.compile(r".*?(\d+\.\d°[EW]):\s+(\d+)\s+([RLHV]).*")
|
||||
self._TR_PAT = re.compile(
|
||||
r".*?(\d+)\s+([RLHV]).*(DVB-S[2]?)/?(.*PSK)?\s(T2-MI)?\s?SR-FEC:\s(\d+)-(\d/\d)\s+.*ONID-TID:\s+(\d+)-(\d+).*")
|
||||
self._POS_PAT = re.compile(r".*?(\d+\.\d°[EW]).*")
|
||||
self._TR = "s {}000:{}000:{}:{}:{}:{}:{}:{}"
|
||||
self._S2_TR = "{}:{}:{}:{}"
|
||||
|
||||
@@ -406,26 +440,33 @@ class ServicesParser(HTMLParser):
|
||||
else:
|
||||
pos, freq, sr, fec, pol, namespace, tid, nid = sat_position or 0, 0, 0, 0, 0, 0, 0, 0
|
||||
sys = "DVB-S"
|
||||
tr_found = False
|
||||
pos_found = False
|
||||
tr = None
|
||||
# Transponder
|
||||
for r in filter(lambda x: x and len(x) == 2, self._rows):
|
||||
for r in filter(lambda x: x and 6 < len(x) < 9, self._rows):
|
||||
if not pos_found:
|
||||
pos_tr = re.match(self._PTR_PAT, r[1].text)
|
||||
if pos_tr:
|
||||
if not sat_position:
|
||||
pos = int(SatellitesParser.get_position(
|
||||
"".join(c for c in pos_tr.group(1) if c.isdigit() or c.isalpha())))
|
||||
freq = int(pos_tr.group(2))
|
||||
pol = get_key_by_value(POLARIZATION, pos_tr.group(3))
|
||||
pos_found = True
|
||||
pos_tr = re.match(self._POS_PAT, r[0].text)
|
||||
if not pos_tr:
|
||||
continue
|
||||
|
||||
if pos_found and not tr_found:
|
||||
td = re.match(self._TR_PAT, r[1].text) or re.match(self._TR_PAT, r[0].text)
|
||||
if not sat_position:
|
||||
pos = int(SatellitesParser.get_position(
|
||||
"".join(c for c in pos_tr.group(1) if c.isdigit() or c.isalpha())))
|
||||
|
||||
pos_found = True
|
||||
|
||||
if pos_found:
|
||||
text = " ".join(c.text for c in r[1:])
|
||||
td = re.match(self._TR_PAT, text)
|
||||
if td:
|
||||
sys, mod, sr, _fec, nid, tid = td.group(1), td.group(2), td.group(3), td.group(4), td.group(
|
||||
5), td.group(6)
|
||||
freq, pol = int(td.group(1)), get_key_by_value(POLARIZATION, td.group(2))
|
||||
if td.group(5):
|
||||
log("Detected T2-MI transponder!")
|
||||
continue
|
||||
|
||||
sys, mod, sr, _fec, = td.group(3), td.group(4), td.group(6), td.group(7)
|
||||
nid, tid = td.group(8), td.group(9)
|
||||
|
||||
neg_pos = False # POS = W
|
||||
# For negative (West) positions: 3600 - numeric position value!!!
|
||||
namespace = "{:04x}0000".format(3600 - pos if neg_pos else pos)
|
||||
@@ -439,7 +480,6 @@ class ServicesParser(HTMLParser):
|
||||
s2_flags = "" if sys == "DVB-S" else self._S2_TR.format(tr_flag, mod or 0, roll_off, pilot)
|
||||
nid, tid = int(nid), int(tid)
|
||||
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
|
||||
tr_found = True
|
||||
|
||||
if not tr:
|
||||
msg = "ServicesParser error [get transponder services]: {}"
|
||||
@@ -449,8 +489,8 @@ class ServicesParser(HTMLParser):
|
||||
|
||||
# Services
|
||||
for r in filter(lambda x: x and len(x) == 12 and (x[0].text.isdigit()), self._rows):
|
||||
sid, name, cas, pkg, s_type, v_pid, a_pid = r[0].text, r[2].text, r[4].text, r[5].text, r[
|
||||
6].text.strip(), r[7].text, r[8].text.split()
|
||||
sid, name, s_type, v_pid, a_pid, cas, pkg = r[0].text, r[2].text, r[4].text, r[
|
||||
5].text.strip(), r[6].text.split(), r[9].text, r[10].text.strip()
|
||||
|
||||
try:
|
||||
s_type = self._S_TYPES.get(s_type, "3") # 3 = Data
|
||||
@@ -469,27 +509,8 @@ class ServicesParser(HTMLParser):
|
||||
else:
|
||||
flags = ",".join(filter(None, (flags, cas)))
|
||||
|
||||
srv = Service(flags_cas=flags,
|
||||
transponder_type="s",
|
||||
coded=None,
|
||||
service=name,
|
||||
locked=None,
|
||||
hide=None,
|
||||
package=pkg,
|
||||
service_type=_s_type,
|
||||
picon=r[1].img,
|
||||
picon_id=picon_id,
|
||||
ssid=sid,
|
||||
freq=freq,
|
||||
rate=sr,
|
||||
pol=pol,
|
||||
fec=fec,
|
||||
system=sys,
|
||||
pos=pos,
|
||||
data_id=data_id,
|
||||
fav_id=fav_id,
|
||||
transponder=tr)
|
||||
services.append(srv)
|
||||
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, r[1].img, picon_id,
|
||||
sid, freq, sr, pol, fec, sys, pos, data_id, fav_id, tr))
|
||||
except ValueError as e:
|
||||
log("ServicesParser error [get transponder services]: {}".format(e))
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ import re
|
||||
import shutil
|
||||
import sys
|
||||
from html.parser import HTMLParser
|
||||
from json import JSONDecodeError
|
||||
from urllib.error import URLError
|
||||
from urllib.parse import unquote
|
||||
from urllib.request import Request, urlopen, urlretrieve
|
||||
|
||||
from app.commons import log
|
||||
from app.settings import SEP
|
||||
from app.ui.uicommons import show_notification
|
||||
|
||||
_YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=)([\w-]{11}).*")
|
||||
@@ -100,7 +100,7 @@ class YouTube:
|
||||
if player_resp:
|
||||
try:
|
||||
resp = json.loads(player_resp)
|
||||
except JSONDecodeError as e:
|
||||
except Exception as e:
|
||||
log("{}: Parsing player response error: {}".format(__class__.__name__, e))
|
||||
else:
|
||||
det = resp.get("videoDetails", None)
|
||||
@@ -129,6 +129,21 @@ class YouTube:
|
||||
|
||||
return None, rsn
|
||||
|
||||
def get_yt_playlist(self, list_id, url=None):
|
||||
""" Returns tuple from the playlist header and list of tuples (title, video id). """
|
||||
if self._settings.enable_yt_dl and url:
|
||||
try:
|
||||
self._yt_dl.update_options({"noplaylist": False, "extract_flat": True})
|
||||
info = self._yt_dl.get_info(url, skip_errors=False)
|
||||
if "url" in info:
|
||||
info = self._yt_dl.get_info(info.get("url"), skip_errors=False)
|
||||
return info.get("title", ""), [(e.get("title", ""), e.get("id", "")) for e in info.get("entries", [])]
|
||||
finally:
|
||||
# Restoring default options
|
||||
self._yt_dl.update_options({"noplaylist": True, "extract_flat": False})
|
||||
|
||||
return PlayListParser.get_yt_playlist(list_id)
|
||||
|
||||
|
||||
class PlayListParser(HTMLParser):
|
||||
""" Very simple parser to handle YouTube playlist pages. """
|
||||
@@ -139,6 +154,7 @@ class PlayListParser(HTMLParser):
|
||||
self._header = ""
|
||||
self._playlist = []
|
||||
self._is_script = False
|
||||
self._scr_start = ('var ytInitialData = ', 'window["ytInitialData"] = ')
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "script":
|
||||
@@ -147,11 +163,14 @@ class PlayListParser(HTMLParser):
|
||||
def handle_data(self, data):
|
||||
if self._is_script:
|
||||
data = data.lstrip()
|
||||
if data.startswith('window["ytInitialData"] = '):
|
||||
data = data.split(";")[0].lstrip('window["ytInitialData"] = ')
|
||||
if data.startswith(self._scr_start):
|
||||
data = data.split(";")[0]
|
||||
for s in self._scr_start:
|
||||
data = data.lstrip(s)
|
||||
|
||||
try:
|
||||
resp = json.loads(data)
|
||||
except JSONDecodeError as e:
|
||||
except YouTubeException as e:
|
||||
log("{}: Parsing data error: {}".format(__class__.__name__, e))
|
||||
else:
|
||||
sb = resp.get("sidebar", None)
|
||||
@@ -205,12 +224,13 @@ class YouTubeDL:
|
||||
_DownloadError = None
|
||||
_LATEST_RELEASE_URL = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest"
|
||||
_OPTIONS = {"noplaylist": True, # Single video instead of a playlist [ignoring playlist in URL].
|
||||
"extract_flat": False, # Do not resolve URLs, return the immediate result.
|
||||
"quiet": True, # Do not print messages to stdout.
|
||||
"simulate": True, # Do not download the video files.
|
||||
"cookiefile": "cookies.txt"} # File name where cookies should be read from and dumped to.
|
||||
|
||||
def __init__(self, settings, callback):
|
||||
self._path = settings.default_data_path + "tools/"
|
||||
self._path = settings.default_data_path + "tools{}".format(SEP)
|
||||
self._update = settings.enable_yt_dl_update
|
||||
self._supported = {"22", "18"}
|
||||
self._dl = None
|
||||
@@ -227,7 +247,7 @@ class YouTubeDL:
|
||||
return cls._DL_INSTANCE
|
||||
|
||||
def init(self):
|
||||
if not os.path.isfile(self._path + "youtube_dl/version.py"):
|
||||
if not os.path.isfile(self._path + "youtube_dl{}version.py".format(SEP)):
|
||||
self.get_latest_release()
|
||||
|
||||
if self._path not in sys.path:
|
||||
@@ -294,7 +314,7 @@ class YouTubeDL:
|
||||
os.makedirs(os.path.dirname(self._path), exist_ok=True)
|
||||
|
||||
for info in arch.infolist():
|
||||
pref, sep, f = info.filename.partition("/youtube_dl/")
|
||||
pref, sep, f = info.filename.partition("{}youtube_dl{}".format(SEP, SEP))
|
||||
if sep:
|
||||
arch.extract(info.filename)
|
||||
shutil.move(info.filename, "{}{}{}".format(self._path, sep, f))
|
||||
@@ -316,8 +336,17 @@ class YouTubeDL:
|
||||
self._callback("Update process. Please wait.", False)
|
||||
return {}, ""
|
||||
|
||||
info = self.get_info(url, skip_errors)
|
||||
fmts = info.get("formats", None)
|
||||
if fmts:
|
||||
return {Quality.get(int(fm["format_id"])): fm.get("url", "") for fm in fmts if
|
||||
fm.get("format_id", "") in self._supported}, info.get("title", "")
|
||||
|
||||
return {}, info.get("title", "")
|
||||
|
||||
def get_info(self, url, skip_errors=False):
|
||||
try:
|
||||
info = self._dl.extract_info(url, download=False)
|
||||
return self._dl.extract_info(url, download=False)
|
||||
except URLError as e:
|
||||
log(str(e))
|
||||
raise YouTubeException(e)
|
||||
@@ -325,13 +354,13 @@ class YouTubeDL:
|
||||
log(str(e))
|
||||
if not skip_errors:
|
||||
raise YouTubeException(e)
|
||||
else:
|
||||
fmts = info.get("formats", None)
|
||||
if fmts:
|
||||
return {Quality.get(int(fm["format_id"])): fm.get("url", "") for fm in fmts if
|
||||
fm.get("format_id", "") in self._supported}, info.get("title", "")
|
||||
|
||||
return {}, info.get("title", "")
|
||||
def update_options(self, options):
|
||||
self._dl.params.update(options)
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
return self._dl.params
|
||||
|
||||
|
||||
def flat(key, d):
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<menu id="app-menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">About</attribute>
|
||||
<attribute name="action">app.on_about_app</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Settings</attribute>
|
||||
<attribute name="action">app.on_settings</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Exit</attribute>
|
||||
<attribute name="action">app.on_close_app</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<menu id="menu_bar">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">File</attribute>
|
||||
@@ -66,19 +46,31 @@
|
||||
<attribute name="action">app.on_download</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Settings</attribute>
|
||||
<attribute name="action">app.on_settings</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Exit</attribute>
|
||||
<attribute name="action">app.on_close_app</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Edit</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Lock</attribute>
|
||||
<attribute name="action">app.on_locked</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Hide</attribute>
|
||||
<attribute name="action">app.on_hide</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Lock</attribute>
|
||||
<attribute name="action">app.on_locked</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Hide</attribute>
|
||||
<attribute name="action">app.on_hide</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">View</attribute>
|
||||
@@ -111,39 +103,50 @@
|
||||
</section>
|
||||
<section id="telnet_section">
|
||||
</section>
|
||||
<section>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">IPTV</attribute>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Add IPTV or stream service</attribute>
|
||||
<attribute name="action">app.on_iptv</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import YouTube playlist</attribute>
|
||||
<attribute name="action">app.on_import_yt_list</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import m3u</attribute>
|
||||
<attribute name="action">app.on_import_m3u</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Export to m3u</attribute>
|
||||
<attribute name="action">app.on_export_to_m3u</attribute>
|
||||
</item>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">EPG configuration</attribute>
|
||||
<attribute name="action">app.on_epg_list_configuration</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">List configuration</attribute>
|
||||
<attribute name="action">app.on_iptv_list_configuration</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Remove all unavailable</attribute>
|
||||
<attribute name="action">app.on_remove_all_unavailable</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">IPTV</attribute>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Add IPTV or stream service</attribute>
|
||||
<attribute name="action">app.on_iptv</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import YouTube playlist</attribute>
|
||||
<attribute name="action">app.on_import_yt_list</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import m3u</attribute>
|
||||
<attribute name="action">app.on_import_m3u</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Export to m3u</attribute>
|
||||
<attribute name="action">app.on_export_to_m3u</attribute>
|
||||
</item>
|
||||
<attribute name="label" translatable="yes">Help</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">EPG configuration</attribute>
|
||||
<attribute name="action">app.on_epg_list_configuration</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">List configuration</attribute>
|
||||
<attribute name="action">app.on_iptv_list_configuration</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Remove all unavailable</attribute>
|
||||
<attribute name="action">app.on_remove_all_unavailable</attribute>
|
||||
<attribute name="label" translatable="yes">About</attribute>
|
||||
<attribute name="action">app.on_about_app</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
|
||||
@@ -57,7 +57,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="Primary"/>
|
||||
<accelerator key="r" signal="activate" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@@ -67,7 +67,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_restore_all" swapped="no"/>
|
||||
<accelerator key="e" signal="activate" modifiers="Primary"/>
|
||||
<accelerator key="e" signal="activate" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@@ -297,7 +297,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="always_show_image">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
|
||||
<accelerator key="i" signal="clicked" modifiers="Primary"/>
|
||||
<accelerator key="i" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
||||
@@ -431,7 +431,9 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="standby_button">
|
||||
@@ -1176,7 +1178,7 @@ audio-volume-medium-symbolic</property>
|
||||
<object class="GtkEntry" id="timer_service_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
@@ -1187,7 +1189,7 @@ audio-volume-medium-symbolic</property>
|
||||
<object class="GtkEntry" id="timer_desc_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
@@ -1198,7 +1200,7 @@ audio-volume-medium-symbolic</property>
|
||||
<object class="GtkEntry" id="timer_name_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
@@ -1485,7 +1487,7 @@ audio-volume-medium-symbolic</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
@@ -1556,7 +1558,7 @@ audio-volume-medium-symbolic</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
@@ -1605,7 +1607,7 @@ audio-volume-medium-symbolic</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="timer_location_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="visible">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-edit-symbolic</property>
|
||||
</object>
|
||||
@@ -1638,7 +1640,7 @@ audio-volume-medium-symbolic</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<property name="placeholder_text" translatable="yes">Default</property>
|
||||
</object>
|
||||
<packing>
|
||||
|
||||
@@ -6,10 +6,12 @@ from urllib.parse import quote
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from .dialogs import get_dialogs_string, show_dialog, DialogType
|
||||
from app.settings import IS_WIN
|
||||
from .dialogs import get_dialogs_string, show_dialog, DialogType, get_message
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column
|
||||
from ..commons import run_task, run_with_delay, log, run_idle
|
||||
from ..connections import HttpAPI
|
||||
from ..eparser.ecommons import BqServiceType
|
||||
|
||||
|
||||
class ControlBox(Gtk.HBox):
|
||||
@@ -60,19 +62,19 @@ class ControlBox(Gtk.HBox):
|
||||
|
||||
@property
|
||||
def event_data(self):
|
||||
return self._event_data
|
||||
return self._event_data or {}
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self._title
|
||||
return self._title or ""
|
||||
|
||||
@property
|
||||
def desc(self):
|
||||
return self._desc
|
||||
return self._desc or ""
|
||||
|
||||
@property
|
||||
def time_header(self):
|
||||
return self._time_header
|
||||
return self._time_header or ""
|
||||
|
||||
class TimerRow(Gtk.ListBoxRow):
|
||||
|
||||
@@ -186,10 +188,6 @@ class ControlBox(Gtk.HBox):
|
||||
self._timers_list_box.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
|
||||
self._timers_list_box.drag_dest_add_text_targets()
|
||||
|
||||
builder.get_object("stack_switcher").set_visible(settings.is_enable_experimental)
|
||||
builder.get_object("epg_box").set_visible(settings.is_enable_experimental)
|
||||
builder.get_object("timers_box").set_visible(settings.is_enable_experimental)
|
||||
|
||||
self.init_actions(app)
|
||||
self.connect("hide", self.on_hide)
|
||||
self.show()
|
||||
@@ -316,18 +314,17 @@ class ControlBox(Gtk.HBox):
|
||||
|
||||
img = data.get("img_data", None)
|
||||
if img:
|
||||
is_darwin = self._settings.is_darwin
|
||||
GLib.idle_add(self._screenshot_button_box.set_sensitive, is_darwin)
|
||||
path = os.path.expanduser("~/Desktop") if is_darwin else None
|
||||
GLib.idle_add(self._screenshot_button_box.set_sensitive, IS_WIN)
|
||||
path = os.path.expanduser("~/Desktop") if IS_WIN else None
|
||||
|
||||
try:
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="wb", suffix=".jpg", dir=path, delete=not is_darwin) as tf:
|
||||
with tempfile.NamedTemporaryFile(mode="wb", suffix=".jpg", dir=path, delete=not IS_WIN) as tf:
|
||||
tf.write(img)
|
||||
cmd = ["open" if is_darwin else "xdg-open", tf.name]
|
||||
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
f_name = tf.name
|
||||
subprocess.Popen([f_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate()
|
||||
finally:
|
||||
GLib.idle_add(self._screenshot_button_box.set_sensitive, True)
|
||||
|
||||
@@ -343,7 +340,7 @@ class ControlBox(Gtk.HBox):
|
||||
|
||||
def on_service_changed(self, ref):
|
||||
self._app._wait_dialog.show()
|
||||
self._http_api.send(HttpAPI.Request.EPG, ref, self.update_epg_data)
|
||||
self._http_api.send(HttpAPI.Request.EPG, quote(ref), self.update_epg_data)
|
||||
|
||||
@run_idle
|
||||
def update_epg_data(self, epg):
|
||||
@@ -360,7 +357,7 @@ class ControlBox(Gtk.HBox):
|
||||
def on_epg_filter_changed(self, entry):
|
||||
self._epg_list_box.invalidate_filter()
|
||||
|
||||
def epg_filter_function(self, row: EpgRow):
|
||||
def epg_filter_function(self, row):
|
||||
txt = self._epg_filter_entry.get_text().upper()
|
||||
return any((not txt, txt in row.time_header.upper(), txt in row.title.upper(), txt in row.desc.upper()))
|
||||
|
||||
@@ -427,7 +424,7 @@ class ControlBox(Gtk.HBox):
|
||||
refs = {}
|
||||
for row in rows:
|
||||
timer = row.timer
|
||||
ref = "timerdelete?sRef={}&begin={}&end={}".format(timer.get("e2servicereference", ""),
|
||||
ref = "timerdelete?sRef={}&begin={}&end={}".format(quote(timer.get("e2servicereference", "")),
|
||||
timer.get("e2timebegin", ""),
|
||||
timer.get("e2timeend", ""))
|
||||
refs[ref] = row
|
||||
@@ -488,7 +485,7 @@ class ControlBox(Gtk.HBox):
|
||||
def on_timer_save(self, action, value=None):
|
||||
args = []
|
||||
t_data = self.get_timer_data()
|
||||
s_ref = t_data.get("sRef", "")
|
||||
s_ref = quote(t_data.get("sRef", ""))
|
||||
|
||||
if self._timer_action is self.TimerAction.EVENT:
|
||||
args.append("timeraddbyeventid?sRef={}".format(s_ref))
|
||||
@@ -673,6 +670,12 @@ class ControlBox(Gtk.HBox):
|
||||
|
||||
service = self._app.current_services.get(fav_id, None)
|
||||
if service:
|
||||
if service.service_type == BqServiceType.ALT.name:
|
||||
msg = "Alternative service.\n\n {}".format(get_message("Not implemented yet!"))
|
||||
show_dialog(DialogType.ERROR, transient=self._app._main_window, text=msg)
|
||||
context.finish(False, False, time)
|
||||
return
|
||||
|
||||
self._timer_name_entry.set_text(service.service)
|
||||
self._timer_service_entry.set_text(service.service)
|
||||
self._timer_service_ref_entry.set_text(service.picon_id.rstrip(".png").replace("_", ":"))
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
* {
|
||||
-GtkDialog-action-area-border: 5em;
|
||||
}
|
||||
|
||||
entry {
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 1.5em;
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
spinbutton {
|
||||
min-height: 1.5em;
|
||||
}
|
||||
|
||||
toolbutton {
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
spinner {
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
infobar {
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
switch slider {
|
||||
min-height: 1.5em;
|
||||
min-width: 1.5em;
|
||||
}
|
||||
|
||||
paned > separator {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 1px 24px;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for macOS. -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkAboutDialog" id="about_dialog">
|
||||
<property name="can_focus">False</property>
|
||||
@@ -40,16 +40,16 @@ 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">1.0.2 Beta</property>
|
||||
<property name="copyright">2018-2020 Dmitriy Yefremov
|
||||
</property>
|
||||
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor for MacOS.
|
||||
(Experimental)</property>
|
||||
<property name="website">https://github.com/DYefremov/DemonEditor/tree/experimental-mac</property>
|
||||
<property name="version">1.0.7 Alpha</property>
|
||||
<property name="copyright">2018-2021 Dmitriy Yefremov
|
||||
</property>
|
||||
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor for MS Windows.
|
||||
(Experimental)</property>
|
||||
<property name="website">https://github.com/DYefremov/DemonEditor/tree/experimental-win</property>
|
||||
<property name="license" translatable="yes">Это приложение распространяется без каких-либо гарантий.
|
||||
Подробнее в <a href="http://opensource.org/licenses/mit-license.php">The MIT License (MIT)</a>.</property>
|
||||
Подробнее в <a href="http://opensource.org/licenses/mit-license.php">The MIT License (MIT)</a>.</property>
|
||||
<property name="authors">Dmitriy Yefremov
|
||||
</property>
|
||||
</property>
|
||||
<property name="translator_credits" translatable="yes">translator-credits</property>
|
||||
<property name="artists">Program logo: <a href="http://ihad.tv">mfgeg</a></property>
|
||||
<property name="logo_icon_name">demon-editor</property>
|
||||
@@ -89,7 +89,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="window_position">center</property>
|
||||
<property name="default_width">320</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="type_hint">utility</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<property name="gravity">center</property>
|
||||
@@ -126,6 +126,7 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkButton" id="input_dialog_ok_button">
|
||||
<property name="label" translatable="yes">OK</property>
|
||||
<property name="width_request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
@@ -153,7 +154,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="margin_right">2</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="margin_bottom">2</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_sensitive">False</property>
|
||||
@@ -182,9 +183,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<property name="decorated">False</property>
|
||||
<property name="gravity">center</property>
|
||||
<child type="titlebar">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="wait_dialog_box">
|
||||
<property name="width_request">100</property>
|
||||
@@ -222,7 +220,7 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</child> <!-- NOP -->
|
||||
<style>
|
||||
<class name="app-notification"/>
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,7 @@ from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from app.commons import run_idle
|
||||
from app.settings import SEP
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN, IS_GNOME_SESSION
|
||||
|
||||
|
||||
@@ -17,7 +18,7 @@ class Dialog(Enum):
|
||||
<property name="use-header-bar">{use_header}</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="default_width">320</property>
|
||||
<property name="width_request">250</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
@@ -74,12 +75,12 @@ class WaitDialog:
|
||||
|
||||
|
||||
def show_dialog(dialog_type, transient, text=None, settings=None, action_type=None, file_filter=None, buttons=None,
|
||||
title=None):
|
||||
title=None, create_dir=False):
|
||||
""" Shows dialogs by name. """
|
||||
if dialog_type in (DialogType.INFO, DialogType.ERROR):
|
||||
return get_message_dialog(transient, dialog_type, Gtk.ButtonsType.OK, text)
|
||||
elif dialog_type is DialogType.CHOOSER and settings:
|
||||
return get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons, title)
|
||||
return get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons, title, create_dir)
|
||||
elif dialog_type is DialogType.INPUT:
|
||||
return get_input_dialog(transient, text)
|
||||
elif dialog_type is DialogType.QUESTION:
|
||||
@@ -103,11 +104,13 @@ def get_chooser_dialog(transient, settings, name, patterns, title=None):
|
||||
title=title)
|
||||
|
||||
|
||||
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons=None, title=None):
|
||||
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons=None, title=None, dirs=False):
|
||||
text = get_message(text) if text else ""
|
||||
action_type = Gtk.FileChooserAction.SELECT_FOLDER if action_type is None else action_type
|
||||
dialog = Gtk.FileChooserNative.new(get_message(title) if title else "", transient, action_type)
|
||||
dialog.set_create_folders(False)
|
||||
dialog.set_modal(True)
|
||||
buttons = buttons or (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
|
||||
dialog = Gtk.FileChooserDialog(text, transient, action_type, buttons, use_header_bar=IS_GNOME_SESSION)
|
||||
dialog.set_title(get_message(title) if title else "")
|
||||
dialog.set_create_folders(dirs)
|
||||
|
||||
if file_filter is not None:
|
||||
dialog.add_filter(file_filter)
|
||||
@@ -115,10 +118,10 @@ def get_file_chooser_dialog(transient, text, settings, action_type, file_filter,
|
||||
dialog.set_current_folder(settings.data_local_path)
|
||||
response = dialog.run()
|
||||
|
||||
if response == Gtk.ResponseType.ACCEPT:
|
||||
if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
path = Path(dialog.get_filename() or dialog.get_current_folder())
|
||||
if path.is_dir():
|
||||
response = "{}/".format(path.resolve())
|
||||
response = "{}{}".format(path.resolve(), SEP)
|
||||
elif path.is_file():
|
||||
response = str(path.resolve())
|
||||
dialog.destroy()
|
||||
@@ -179,7 +182,7 @@ def get_message(message):
|
||||
|
||||
@lru_cache(maxsize=5)
|
||||
def get_dialogs_string(path):
|
||||
with open(path, "r") as f:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return "".join(f)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -46,7 +46,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkWindow" id="download_dialog_window">
|
||||
<property name="width_request">500</property>
|
||||
<property name="width_request">550</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">FTP-transfer</property>
|
||||
<property name="resizable">False</property>
|
||||
@@ -56,7 +56,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<property name="gravity">center</property>
|
||||
<child>
|
||||
<child type="titlebar">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
@@ -231,202 +231,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="settings_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="label_xalign">0.019999999552965164</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="settings_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">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="row_spacing">2</property>
|
||||
<property name="column_spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="login_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Login:</property>
|
||||
<property name="xalign">0.10000000149011612</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="login_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="text">root</property>
|
||||
<property name="caps_lock_warning">False</property>
|
||||
<property name="primary_icon_name">avatar-default-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="password_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Password:</property>
|
||||
<property name="xalign">0.10000000149011612</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="password_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="visibility">False</property>
|
||||
<property name="invisible_char">●</property>
|
||||
<property name="text">root</property>
|
||||
<property name="primary_icon_name">emblem-readonly</property>
|
||||
<property name="input_purpose">password</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="port_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Port:</property>
|
||||
<property name="xalign">0.10000000149011612</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="port_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="width_chars">8</property>
|
||||
<property name="max_width_chars">8</property>
|
||||
<property name="text" translatable="yes">21</property>
|
||||
<property name="caps_lock_warning">False</property>
|
||||
<property name="primary_icon_name">network-workgroup-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="timeout_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="width_chars">8</property>
|
||||
<property name="max_width_chars">8</property>
|
||||
<property name="caps_lock_warning">False</property>
|
||||
<property name="primary_icon_name">alarm-symbolic</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="timeout_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Timeout:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkBox" id="settings_buttons_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="ftp_radio_button">
|
||||
<property name="label" translatable="yes">FTP</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<property name="group">telnet_radio_button</property>
|
||||
<signal name="toggled" handler="on_settings_button" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="http_radio_button">
|
||||
<property name="label" translatable="yes">HTTP</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">telnet_radio_button</property>
|
||||
<signal name="toggled" handler="on_settings_button" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="telnet_radio_button">
|
||||
<property name="label" translatable="yes">Telnet</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">ftp_radio_button</property>
|
||||
<signal name="toggled" handler="on_settings_button" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</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">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="visible">True</property>
|
||||
@@ -568,7 +372,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Options</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_settings" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -748,11 +551,4 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkSizeGroup" id="settings_size_group">
|
||||
<widgets>
|
||||
<widget name="ftp_radio_button"/>
|
||||
<widget name="http_radio_button"/>
|
||||
<widget name="telnet_radio_button"/>
|
||||
</widgets>
|
||||
</object>
|
||||
</interface>
|
||||
|
||||
@@ -21,7 +21,6 @@ class DownloadDialog:
|
||||
|
||||
handlers = {"on_receive": self.on_receive,
|
||||
"on_send": self.on_send,
|
||||
"on_settings_button": self.on_settings_button,
|
||||
"on_settings": self.on_settings,
|
||||
"on_profile_changed": self.on_profile_changed,
|
||||
"on_use_http_state_set": self.on_use_http_state_set,
|
||||
@@ -32,7 +31,6 @@ class DownloadDialog:
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "download_dialog.glade")
|
||||
builder.connect_signals(handlers)
|
||||
|
||||
self._current_property = "FTP"
|
||||
self._dialog_window = builder.get_object("download_dialog_window")
|
||||
self._dialog_window.set_transient_for(transient)
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
@@ -46,12 +44,6 @@ class DownloadDialog:
|
||||
self._bouquets_radio_button = builder.get_object("bouquets_radio_button")
|
||||
self._satellites_radio_button = builder.get_object("satellites_radio_button")
|
||||
self._webtv_radio_button = builder.get_object("webtv_radio_button")
|
||||
self._login_entry = builder.get_object("login_entry")
|
||||
self._password_entry = builder.get_object("password_entry")
|
||||
self._host_entry = builder.get_object("host_entry")
|
||||
self._port_entry = builder.get_object("port_entry")
|
||||
self._timeout_entry = builder.get_object("timeout_entry")
|
||||
self._settings_buttons_box = builder.get_object("settings_buttons_box")
|
||||
self._use_http_switch = builder.get_object("use_http_switch")
|
||||
self._http_radio_button = builder.get_object("http_radio_button")
|
||||
self._use_http_box = builder.get_object("use_http_box")
|
||||
@@ -71,7 +63,6 @@ class DownloadDialog:
|
||||
self._data_path_entry.set_text(self._settings.data_local_path)
|
||||
is_enigma = self._s_type is SettingsType.ENIGMA_2
|
||||
self._webtv_radio_button.set_visible(not is_enigma)
|
||||
self._http_radio_button.set_visible(is_enigma)
|
||||
self._use_http_box.set_visible(is_enigma)
|
||||
self._use_http_switch.set_active(is_enigma and self._settings.use_http)
|
||||
self._remove_unused_check_button.set_active(self._settings.remove_unused_bouquets)
|
||||
@@ -104,26 +95,6 @@ class DownloadDialog:
|
||||
def destroy(self):
|
||||
self._dialog_window.destroy()
|
||||
|
||||
def on_settings_button(self, button):
|
||||
if button.get_active():
|
||||
label = button.get_label()
|
||||
if label == "Telnet":
|
||||
self._login_entry.set_text(self._settings.telnet_user)
|
||||
self._password_entry.set_text(self._settings.telnet_password)
|
||||
self._port_entry.set_text(self._settings.telnet_port)
|
||||
self._timeout_entry.set_text(str(self._settings.telnet_timeout))
|
||||
elif label == "HTTP":
|
||||
self._login_entry.set_text(self._settings.http_user)
|
||||
self._password_entry.set_text(self._settings.http_password)
|
||||
self._port_entry.set_text(self._settings.http_port)
|
||||
self._timeout_entry.set_text(str(self._settings.http_timeout))
|
||||
elif label == "FTP":
|
||||
self._login_entry.set_text(self._settings.user)
|
||||
self._password_entry.set_text(self._settings.password)
|
||||
self._port_entry.set_text(self._settings.port)
|
||||
self._timeout_entry.set_text("")
|
||||
self._current_property = label
|
||||
|
||||
def on_settings(self, item):
|
||||
response = show_settings_dialog(self._dialog_window, self._settings)
|
||||
if response != Gtk.ResponseType.CANCEL:
|
||||
@@ -132,11 +103,6 @@ class DownloadDialog:
|
||||
gen = self._update_settings_callback()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
for button in self._settings_buttons_box.get_children():
|
||||
if button.get_active():
|
||||
self.on_settings_button(button)
|
||||
break
|
||||
|
||||
def on_profile_changed(self, box):
|
||||
active = box.get_active_text()
|
||||
if active in self._settings.profiles:
|
||||
|
||||
@@ -87,7 +87,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="image">copy_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_copy_ref" swapped="no"/>
|
||||
<accelerator key="c" signal="activate" modifiers="Primary"/>
|
||||
<accelerator key="c" signal="activate" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@@ -118,7 +118,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="image">insert_link_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_assign_ref" swapped="no"/>
|
||||
<accelerator key="v" signal="activate" modifiers="Primary"/>
|
||||
<accelerator key="v" signal="activate" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@@ -436,7 +436,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="text" translatable="yes">/data/epg/</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<property name="secondary_icon_name">folder-open-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_tooltip_text" translatable="yes">Select</property>
|
||||
@@ -466,7 +466,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="text" translatable="yes">/etc/enigma2/</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
|
||||
@@ -8,8 +8,7 @@ from enum import Enum
|
||||
from urllib.error import HTTPError, URLError
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_idle
|
||||
from app.commons import run_idle, run_task
|
||||
from app.connections import download_data, DownloadType
|
||||
from app.eparser.ecommons import BouquetService, BqServiceType
|
||||
from app.tools.epg import EPG, ChannelsParser
|
||||
@@ -186,7 +185,7 @@ class EpgDialog:
|
||||
|
||||
def init_bouquet_data(self):
|
||||
for r in self._ex_fav_model:
|
||||
row = [*r[:]]
|
||||
row = list(r[:])
|
||||
fav_id = r[Column.FAV_ID]
|
||||
self._services[fav_id] = self._ex_services[fav_id].fav_id
|
||||
yield self._bouquet_model.append(row)
|
||||
@@ -539,6 +538,7 @@ class EpgDialog:
|
||||
|
||||
# ***************** Downloads *********************#
|
||||
|
||||
@run_task
|
||||
def download_epg_from_stb(self):
|
||||
""" Download the epg.dat file via ftp from the receiver. """
|
||||
download_data(settings=self._settings, download_type=DownloadType.EPG, callback=print)
|
||||
|
||||
657
app/ui/ftp.glade
Normal file
657
app/ui/ftp.glade
Normal file
@@ -0,0 +1,657 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkListStore" id="bookmarks_list_store">
|
||||
<columns>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name url -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkImage" id="file_create_folder_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">folder-new</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="file_list_store">
|
||||
<columns>
|
||||
<!-- column-name icon -->
|
||||
<column type="GdkPixbuf"/>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name size -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name date -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name type -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name extra -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkImage" id="ftp_create_folder_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">folder-new</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="ftp_list_store">
|
||||
<columns>
|
||||
<!-- column-name icon -->
|
||||
<column type="GdkPixbuf"/>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name size -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name date -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name attr -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name extra -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkFrame" id="main_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">1</property>
|
||||
<property name="margin_right">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkPaned" id="paned">
|
||||
<property name="width_request">320</property>
|
||||
<property name="height_request">240</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</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="orientation">vertical</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="ftp_bpx">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="ftp_info_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ftp_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="label">FTP:</property>
|
||||
<property name="yalign">1</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="semibold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ftp_info_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="max_width_chars">25</property>
|
||||
<property name="yalign">1</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">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="ftp_button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="connect_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Connect</property>
|
||||
<signal name="clicked" handler="on_connect" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="connect_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-connect</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="disconnect_button">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Disconnect</property>
|
||||
<signal name="clicked" handler="on_disconnect" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="disconnect_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-disconnect</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="bookmark_button">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">bookmarks_list_store</property>
|
||||
<property name="id_column">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</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">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="ftp_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="min_content_height">100</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="ftp_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">ftp_list_store</property>
|
||||
<property name="search_column">1</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="ftp_popup_menu" swapped="no"/>
|
||||
<signal name="button-press-event" handler="on_view_press" swapped="no"/>
|
||||
<signal name="button-release-event" handler="on_view_release" swapped="no"/>
|
||||
<signal name="drag-begin" handler="on_view_drag_begin" after="yes" swapped="no"/>
|
||||
<signal name="drag-data-get" handler="on_ftp_drag_data_get" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_ftp_drag_data_received" swapped="no"/>
|
||||
<signal name="drag-end" handler="on_view_drag_end" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_ftp_row_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="ftp_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_name_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="ftp_icon_column_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_name_column_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<signal name="edited" handler="on_ftp_edited" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_size_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Size</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_size_column_renderer">
|
||||
<property name="xalign">0.94999998807907104</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_date_column">
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Date</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_date_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_attr_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">50</property>
|
||||
<property name="title" translatable="yes">Attr.</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">4</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_attr_column_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_extra_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="title" translatable="yes">Extra</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_extra_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</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="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="file_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="pc_info_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="pc_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="label" translatable="yes">PC:</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="semibold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="pc_info_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="max_width_chars">32</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="file_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="min_content_height">100</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="file_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">file_list_store</property>
|
||||
<property name="search_column">1</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="file_popup_menu" swapped="no"/>
|
||||
<signal name="button-press-event" handler="on_view_press" swapped="no"/>
|
||||
<signal name="button-release-event" handler="on_view_release" swapped="no"/>
|
||||
<signal name="drag-begin" handler="on_view_drag_begin" after="yes" swapped="no"/>
|
||||
<signal name="drag-data-get" handler="on_file_drag_data_get" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_file_drag_data_received" swapped="no"/>
|
||||
<signal name="drag-end" handler="on_view_drag_end" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_file_row_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="file_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_name_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="file_icon_column_renderer">
|
||||
<property name="xalign">0.20000000298023224</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_name_column_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
<signal name="edited" handler="on_file_edited" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_size_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Size</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_size_column_renderer">
|
||||
<property name="xalign">0.94999998807907104</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_date_column">
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Date</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_date_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_type_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">50</property>
|
||||
<property name="title" translatable="yes">Path</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_path_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_extra_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="title" translatable="yes">Extra</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_extra_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</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="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">list-remove</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_image_2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">list-remove</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="rename_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">gtk-edit</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="ftp_popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="ftp_create_folder_menu_item">
|
||||
<property name="label" translatable="yes">Create folder</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">ftp_create_folder_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_ftp_create_folder" object="ftp_name_column_renderer" swapped="no"/>
|
||||
<accelerator key="F7" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="ftp_edit_menu_item">
|
||||
<property name="label" translatable="yes">Edit</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">rename_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_ftp_edit" object="ftp_name_column_renderer" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="GDK_CONTROL_MASK"/>
|
||||
<accelerator key="F2" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="ftp_remove_menu_item">
|
||||
<property name="label" translatable="yes">Remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">remove_image_2</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_ftp_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="rename_image_2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">gtk-edit</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="file_popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="file_create_folder_menu_item">
|
||||
<property name="label" translatable="yes">Create folder</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">file_create_folder_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_file_create_folder" object="file_name_column_renderer" swapped="no"/>
|
||||
<accelerator key="F7" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="file_edit_menu_item">
|
||||
<property name="label" translatable="yes">Edit</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">rename_image_2</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_file_edit" object="file_name_column_renderer" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="GDK_CONTROL_MASK"/>
|
||||
<accelerator key="F2" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="file_remove_menu_item">
|
||||
<property name="label" translatable="yes">Remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">remove_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_file_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
574
app/ui/ftp.py
Normal file
574
app/ui/ftp.py
Normal file
@@ -0,0 +1,574 @@
|
||||
""" Simple FTP client module. """
|
||||
import subprocess
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from ftplib import all_errors
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from urllib.parse import urlparse, unquote
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import log, run_task, run_idle
|
||||
from app.connections import UtfFTP
|
||||
from app.ui.dialogs import show_dialog, DialogType
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
|
||||
|
||||
File = namedtuple("File", ["icon", "name", "size", "date", "attr", "extra"])
|
||||
|
||||
|
||||
class FtpClientBox(Gtk.HBox):
|
||||
""" Simple FTP client base class. """
|
||||
ROOT = ".."
|
||||
FOLDER = "<Folder>"
|
||||
LINK = "<Link>"
|
||||
MAX_SIZE = 10485760 # 10 MB file limit
|
||||
URI_SEP = "::::"
|
||||
|
||||
class Column(IntEnum):
|
||||
ICON = 0
|
||||
NAME = 1
|
||||
SIZE = 2
|
||||
DATE = 3
|
||||
ATTR = 4
|
||||
EXTRA = 5
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_spacing(2)
|
||||
self.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
|
||||
self._app = app
|
||||
self._settings = settings
|
||||
self._ftp = None
|
||||
self._select_enabled = True
|
||||
|
||||
handlers = {"on_connect": self.on_connect,
|
||||
"on_disconnect": self.on_disconnect,
|
||||
"on_ftp_row_activated": self.on_ftp_row_activated,
|
||||
"on_file_row_activated": self.on_file_row_activated,
|
||||
"on_ftp_edit": self.on_ftp_edit,
|
||||
"on_ftp_edited": self.on_ftp_edited,
|
||||
"on_file_edit": self.on_file_edit,
|
||||
"on_file_edited": self.on_file_edited,
|
||||
"on_file_remove": self.on_file_remove,
|
||||
"on_ftp_remove": self.on_ftp_file_remove,
|
||||
"on_file_create_folder": self.on_file_create_folder,
|
||||
"on_ftp_create_folder": self.on_ftp_create_folder,
|
||||
"on_view_drag_begin": self.on_view_drag_begin,
|
||||
"on_ftp_drag_data_get": self.on_ftp_drag_data_get,
|
||||
"on_ftp_drag_data_received": self.on_ftp_drag_data_received,
|
||||
"on_file_drag_data_get": self.on_file_drag_data_get,
|
||||
"on_file_drag_data_received": self.on_file_drag_data_received,
|
||||
"on_view_drag_end": self.on_view_drag_end,
|
||||
"on_view_popup_menu": on_popup_menu,
|
||||
"on_view_key_press": self.on_view_key_press,
|
||||
"on_view_press": self.on_view_press,
|
||||
"on_view_release": self.on_view_release}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "ftp.glade")
|
||||
builder.connect_signals(handlers)
|
||||
|
||||
self.add(builder.get_object("main_frame"))
|
||||
self._ftp_info_label = builder.get_object("ftp_info_label")
|
||||
self._ftp_view = builder.get_object("ftp_view")
|
||||
self._ftp_model = builder.get_object("ftp_list_store")
|
||||
self._ftp_name_renderer = builder.get_object("ftp_name_column_renderer")
|
||||
self._file_view = builder.get_object("file_view")
|
||||
self._file_model = builder.get_object("file_list_store")
|
||||
self._file_name_renderer = builder.get_object("file_name_column_renderer")
|
||||
# Buttons
|
||||
self._connect_button = builder.get_object("connect_button")
|
||||
disconnect_button = builder.get_object("disconnect_button")
|
||||
disconnect_button.bind_property("visible", builder.get_object("ftp_create_folder_menu_item"), "sensitive")
|
||||
disconnect_button.bind_property("visible", builder.get_object("ftp_edit_menu_item"), "sensitive")
|
||||
disconnect_button.bind_property("visible", builder.get_object("ftp_remove_menu_item"), "sensitive")
|
||||
self._connect_button.bind_property("visible", builder.get_object("disconnect_button"), "visible", 4)
|
||||
# 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()
|
||||
self.init_file_data()
|
||||
self.show()
|
||||
|
||||
@run_task
|
||||
def init_ftp(self):
|
||||
GLib.idle_add(self._ftp_model.clear)
|
||||
try:
|
||||
if self._ftp:
|
||||
self._ftp.close()
|
||||
|
||||
self._ftp = UtfFTP(host=self._settings.host, 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:
|
||||
self.update_ftp_info(str(e))
|
||||
self.on_disconnect()
|
||||
else:
|
||||
self.init_ftp_data()
|
||||
|
||||
@run_task
|
||||
def init_ftp_data(self, path=None):
|
||||
if not self._ftp:
|
||||
return
|
||||
|
||||
if path:
|
||||
try:
|
||||
self._ftp.cwd(path)
|
||||
except all_errors as e:
|
||||
self.update_ftp_info(str(e))
|
||||
|
||||
files = []
|
||||
try:
|
||||
self._ftp.dir(files.append)
|
||||
except all_errors as e:
|
||||
log(e)
|
||||
self.update_ftp_info(str(e))
|
||||
self.on_disconnect()
|
||||
else:
|
||||
self.append_ftp_data(files)
|
||||
GLib.idle_add(self._connect_button.set_visible, False)
|
||||
|
||||
@run_task
|
||||
def init_file_data(self, path=None):
|
||||
self.append_file_data(Path(path if path else self._settings.data_local_path))
|
||||
|
||||
@run_idle
|
||||
def append_file_data(self, path: Path):
|
||||
self._file_model.clear()
|
||||
self._file_model.append(File(None, self.ROOT, None, None, str(path), "0"))
|
||||
|
||||
try:
|
||||
dirs = [p for p in path.iterdir()]
|
||||
except OSError as e:
|
||||
log(e)
|
||||
else:
|
||||
for p in dirs:
|
||||
is_dir = p.is_dir()
|
||||
st = p.stat()
|
||||
size = str(st.st_size)
|
||||
date = datetime.fromtimestamp(st.st_mtime).strftime("%d-%m-%y %H:%M")
|
||||
icon = None
|
||||
if is_dir:
|
||||
r_size = self.FOLDER
|
||||
icon = self._folder_icon
|
||||
elif p.is_symlink():
|
||||
r_size = self.LINK
|
||||
icon = self._link_icon
|
||||
else:
|
||||
r_size = self.get_size_from_bytes(size)
|
||||
|
||||
self._file_model.append(File(icon, p.name, r_size, date, str(p.resolve()), size))
|
||||
|
||||
@run_idle
|
||||
def append_ftp_data(self, files):
|
||||
self._ftp_model.clear()
|
||||
self._ftp_model.append(File(None, self.ROOT, None, None, self._ftp.pwd(), "0"))
|
||||
|
||||
for f in files:
|
||||
f_data = f.split()
|
||||
f_type = f_data[0][0]
|
||||
is_dir = f_type == "d"
|
||||
is_link = f_type == "l"
|
||||
size = f_data[4]
|
||||
|
||||
icon = None
|
||||
if is_dir:
|
||||
r_size = self.FOLDER
|
||||
icon = self._folder_icon
|
||||
elif is_link:
|
||||
r_size = self.LINK
|
||||
icon = self._link_icon
|
||||
else:
|
||||
r_size = self.get_size_from_bytes(size)
|
||||
|
||||
date = "{}, {} {}".format(f_data[5], f_data[6], f_data[7])
|
||||
self._ftp_model.append(File(icon, " ".join(f_data[8:]), r_size, date, f_data[0], size))
|
||||
|
||||
def on_connect(self, item=None):
|
||||
self.init_ftp()
|
||||
|
||||
def on_disconnect(self, item=None):
|
||||
if self._ftp:
|
||||
self._ftp.close()
|
||||
self._connect_button.set_visible(True)
|
||||
GLib.idle_add(self._ftp_model.clear)
|
||||
|
||||
def on_ftp_row_activated(self, view, path, column):
|
||||
row = self._ftp_model[path][:]
|
||||
f_path = row[self.Column.NAME]
|
||||
size = row[self.Column.SIZE]
|
||||
|
||||
if size == self.FOLDER or f_path == self.ROOT:
|
||||
self.init_ftp_data(f_path)
|
||||
else:
|
||||
b_size = row[self.Column.EXTRA]
|
||||
if b_size.isdigit() and int(b_size) > self.MAX_SIZE:
|
||||
self._app.show_error_dialog("The file size is too large!")
|
||||
else:
|
||||
self.open_ftp_file(f_path)
|
||||
|
||||
def on_file_row_activated(self, view, path, column):
|
||||
row = self._file_model[path][:]
|
||||
path = Path(row[self.Column.ATTR])
|
||||
if row[self.Column.SIZE] == self.FOLDER:
|
||||
self.init_file_data(path)
|
||||
elif row[self.Column.NAME] == self.ROOT:
|
||||
self.init_file_data(path.parent)
|
||||
else:
|
||||
self.open_file(row[self.Column.ATTR])
|
||||
|
||||
@run_task
|
||||
def open_file(self, path):
|
||||
GLib.idle_add(self._file_view.set_sensitive, False)
|
||||
try:
|
||||
cmd = ["open" if self._settings.is_darwin else "xdg-open", path]
|
||||
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
finally:
|
||||
GLib.idle_add(self._file_view.set_sensitive, True)
|
||||
|
||||
@run_task
|
||||
def open_ftp_file(self, f_path):
|
||||
is_darwin = self._settings.is_darwin
|
||||
GLib.idle_add(self._ftp_view.set_sensitive, False)
|
||||
|
||||
try:
|
||||
import tempfile
|
||||
import os
|
||||
path = os.path.expanduser("~/Desktop") if is_darwin else None
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="wb", dir=path, delete=not is_darwin) as tf:
|
||||
msg = "Downloading file: {}. Status: {}"
|
||||
try:
|
||||
status = self._ftp.retrbinary("RETR " + f_path, tf.write)
|
||||
self.update_ftp_info(msg.format(f_path, status))
|
||||
except all_errors as e:
|
||||
self.update_ftp_info(msg.format(f_path, e))
|
||||
|
||||
tf.flush()
|
||||
cmd = ["open" if is_darwin else "xdg-open", tf.name]
|
||||
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
finally:
|
||||
GLib.idle_add(self._ftp_view.set_sensitive, True)
|
||||
|
||||
def on_ftp_edit(self, renderer):
|
||||
model, paths = self._ftp_view.get_selection().get_selected_rows()
|
||||
if not paths:
|
||||
return
|
||||
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_dialog("Please, select only one item!")
|
||||
return
|
||||
|
||||
renderer.set_property("editable", True)
|
||||
self._ftp_view.set_cursor(paths, self._ftp_view.get_column(0), True)
|
||||
|
||||
def on_ftp_edited(self, renderer, path, new_value):
|
||||
renderer.set_property("editable", False)
|
||||
row = self._ftp_model[path]
|
||||
old_name = row[self.Column.NAME]
|
||||
if old_name == new_value:
|
||||
return
|
||||
|
||||
resp = self._ftp.rename_file(old_name, new_value)
|
||||
self.update_ftp_info("{} Status: {}".format(old_name, resp))
|
||||
if resp[0] == "2":
|
||||
row[self.Column.NAME] = new_value
|
||||
|
||||
def on_file_edit(self, renderer):
|
||||
model, paths = self._file_view.get_selection().get_selected_rows()
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_dialog("Please, select only one item!")
|
||||
return
|
||||
|
||||
renderer.set_property("editable", True)
|
||||
self._file_view.set_cursor(paths, self._file_view.get_column(0), True)
|
||||
|
||||
def on_file_edited(self, renderer, path, new_value):
|
||||
renderer.set_property("editable", False)
|
||||
row = self._file_model[path]
|
||||
old_name = row[self.Column.NAME]
|
||||
if old_name == new_value:
|
||||
return
|
||||
|
||||
path = Path(row[self.Column.ATTR])
|
||||
if path.exists():
|
||||
try:
|
||||
new_path = path.rename("{}/{}".format(path.parent, new_value))
|
||||
except ValueError as e:
|
||||
log(e)
|
||||
self._app.show_error_dialog(str(e))
|
||||
else:
|
||||
if new_path.name == new_value:
|
||||
row[self.Column.NAME] = new_value
|
||||
row[self.Column.ATTR] = str(new_path.resolve())
|
||||
|
||||
def on_file_remove(self, item=None):
|
||||
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
model, paths = self._file_view.get_selection().get_selected_rows()
|
||||
to_delete = []
|
||||
|
||||
for path in filter(lambda p: model[p][self.Column.NAME] != self.ROOT, paths):
|
||||
f_path = Path(model[path][self.Column.ATTR])
|
||||
try:
|
||||
rmtree(f_path, ignore_errors=True) if f_path.is_dir() else f_path.unlink()
|
||||
except OSError as e:
|
||||
log(e)
|
||||
else:
|
||||
to_delete.append(model.get_iter(path))
|
||||
|
||||
list(map(model.remove, to_delete))
|
||||
|
||||
def on_ftp_file_remove(self, item=None):
|
||||
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
model, paths = self._ftp_view.get_selection().get_selected_rows()
|
||||
to_delete = []
|
||||
|
||||
for path in filter(lambda p: model[p][self.Column.NAME] != self.ROOT, paths):
|
||||
row = model[path][:]
|
||||
name = row[self.Column.NAME]
|
||||
if row[self.Column.SIZE] == self.FOLDER:
|
||||
resp = self._ftp.delete_dir(name, self.update_ftp_info)
|
||||
else:
|
||||
resp = self._ftp.delete_file(name, self.update_ftp_info)
|
||||
|
||||
if resp[0] == "2":
|
||||
to_delete.append(model.get_iter(path))
|
||||
|
||||
list(map(model.remove, to_delete))
|
||||
|
||||
def on_file_create_folder(self, renderer):
|
||||
itr = self._file_model.get_iter_first()
|
||||
if not itr:
|
||||
return
|
||||
|
||||
name = self.get_new_folder_name(self._file_model)
|
||||
cur_path = self._file_model.get_value(itr, self.Column.ATTR)
|
||||
path = Path("{}/{}".format(cur_path, name))
|
||||
|
||||
try:
|
||||
path.mkdir()
|
||||
except OSError as e:
|
||||
log(e)
|
||||
self._app.show_error_dialog(str(e))
|
||||
else:
|
||||
itr = self._file_model.append(File(self._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)
|
||||
|
||||
def on_ftp_create_folder(self, renderer):
|
||||
itr = self._ftp_model.get_iter_first()
|
||||
if not itr:
|
||||
return
|
||||
|
||||
cur_path = self._ftp_model.get_value(itr, self.Column.ATTR)
|
||||
name = self.get_new_folder_name(self._ftp_model)
|
||||
|
||||
try:
|
||||
folder = "{}/{}".format(cur_path, name)
|
||||
resp = self._ftp.mkd(folder)
|
||||
except all_errors as e:
|
||||
self.update_ftp_info(str(e))
|
||||
log(e)
|
||||
else:
|
||||
if resp == "{}/{}".format(cur_path, name):
|
||||
itr = self._ftp_model.append(File(self._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)
|
||||
|
||||
def get_new_folder_name(self, model):
|
||||
""" Returns the default name for the newly created folder. """
|
||||
name = "new folder"
|
||||
names = {r[self.Column.NAME] for r in model}
|
||||
count = 0
|
||||
while name in names:
|
||||
count += 1
|
||||
name = "{}{}".format(name, count)
|
||||
return name
|
||||
|
||||
# ***************** Drag-and-drop ********************* #
|
||||
|
||||
def init_drag_and_drop(self):
|
||||
self._ftp_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
|
||||
self._ftp_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
|
||||
self._ftp_view.drag_source_add_uri_targets()
|
||||
self._ftp_view.drag_dest_add_uri_targets()
|
||||
|
||||
self._file_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
|
||||
self._file_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
|
||||
self._file_view.drag_source_add_uri_targets()
|
||||
self._file_view.drag_dest_add_uri_targets()
|
||||
|
||||
self._ftp_view.get_selection().set_select_function(lambda *args: self._select_enabled)
|
||||
self._file_view.get_selection().set_select_function(lambda *args: self._select_enabled)
|
||||
|
||||
def on_view_drag_begin(self, view, context):
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if len(paths) < 1:
|
||||
return
|
||||
|
||||
pix = self._app.get_drag_icon_pixbuf(model, paths, self.Column.NAME, self.Column.SIZE)
|
||||
Gtk.drag_set_icon_pixbuf(context, pix, 0, 0)
|
||||
return True
|
||||
|
||||
def on_ftp_drag_data_get(self, view, context, data, info, time):
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if len(paths) > 0:
|
||||
sep = self.URI_SEP if self._settings.is_darwin else "\n"
|
||||
uris = []
|
||||
for r in [model[p][:] for p in paths]:
|
||||
if r[self.Column.SIZE] != self.LINK and r[self.Column.NAME] != self.ROOT:
|
||||
uris.append(Path("/{}:{}".format(r[self.Column.NAME], r[self.Column.ATTR])).as_uri())
|
||||
data.set_uris([sep.join(uris)])
|
||||
|
||||
@run_task
|
||||
def on_ftp_drag_data_received(self, view, context, x, y, data: Gtk.SelectionData, info, time):
|
||||
if not self._ftp:
|
||||
return
|
||||
|
||||
resp = "2"
|
||||
try:
|
||||
GLib.idle_add(self._app._wait_dialog.show)
|
||||
|
||||
uris = data.get_uris()
|
||||
if self._settings.is_darwin and len(uris) == 1:
|
||||
uris = uris[0].split(self.URI_SEP)
|
||||
|
||||
for uri in uris:
|
||||
uri = urlparse(unquote(uri)).path
|
||||
path = Path(uri)
|
||||
if path.is_dir():
|
||||
try:
|
||||
self._ftp.mkd(path.name)
|
||||
except all_errors as e:
|
||||
pass # NOP
|
||||
self._ftp.cwd(path.name)
|
||||
resp = self._ftp.upload_dir(str(path.resolve()) + "/", self.update_ftp_info)
|
||||
else:
|
||||
resp = self._ftp.send_file(path.name, str(path.parent) + "/", callback=self.update_ftp_info)
|
||||
finally:
|
||||
GLib.idle_add(self._app._wait_dialog.hide)
|
||||
if resp and resp[0] == "2":
|
||||
itr = self._ftp_model.get_iter_first()
|
||||
if itr:
|
||||
self.init_ftp_data(self._ftp_model.get_value(itr, self.Column.ATTR))
|
||||
Gtk.drag_finish(context, True, False, time)
|
||||
return True
|
||||
|
||||
def on_file_drag_data_get(self, view, context, data: Gtk.SelectionData, info, time):
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if len(paths) > 0:
|
||||
sep = self.URI_SEP if self._settings.is_darwin else "\n"
|
||||
uris = [sep.join([Path(model[p][self.Column.ATTR]).as_uri() for p in paths])]
|
||||
data.set_uris(uris)
|
||||
|
||||
@run_task
|
||||
def on_file_drag_data_received(self, view, context, x, y, data, info, time):
|
||||
cur_path = self._file_model.get_value(self._file_model.get_iter_first(), self.Column.ATTR) + "/"
|
||||
try:
|
||||
GLib.idle_add(self._app._wait_dialog.show)
|
||||
|
||||
uris = data.get_uris()
|
||||
if self._settings.is_darwin and len(uris) == 1:
|
||||
uris = uris[0].split(self.URI_SEP)
|
||||
|
||||
for uri in uris:
|
||||
name, sep, attr = unquote(Path(uri).name).partition(":")
|
||||
if not attr:
|
||||
return True
|
||||
|
||||
if attr[0] == "d":
|
||||
self._ftp.download_dir(name, cur_path, self.update_ftp_info)
|
||||
else:
|
||||
self._ftp.download_file(name, cur_path, self.update_ftp_info)
|
||||
except OSError as e:
|
||||
log(e)
|
||||
finally:
|
||||
GLib.idle_add(self._app._wait_dialog.hide)
|
||||
self.init_file_data(cur_path)
|
||||
|
||||
Gtk.drag_finish(context, True, False, time)
|
||||
return True
|
||||
|
||||
def on_view_drag_end(self, view, context):
|
||||
self._select_enabled = True
|
||||
view.get_selection().unselect_all()
|
||||
|
||||
@run_idle
|
||||
def update_ftp_info(self, message):
|
||||
message = message.strip()
|
||||
self._ftp_info_label.set_text(message)
|
||||
self._ftp_info_label.set_tooltip_text(message)
|
||||
|
||||
def on_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 key is KeyboardKey.F7:
|
||||
if self._ftp_view.is_focus():
|
||||
self.on_ftp_create_folder(self._ftp_name_renderer)
|
||||
elif self._file_view.is_focus():
|
||||
self.on_file_create_folder(self._file_name_renderer)
|
||||
elif key is KeyboardKey.F2 or ctrl and KeyboardKey.R:
|
||||
if self._ftp_view.is_focus():
|
||||
self.on_ftp_edit(self._ftp_name_renderer)
|
||||
elif self._file_view.is_focus():
|
||||
self.on_file_edit(self._file_name_renderer)
|
||||
elif key is KeyboardKey.DELETE:
|
||||
if self._ftp_view.is_focus():
|
||||
self.on_ftp_file_remove()
|
||||
elif self._file_view.is_focus():
|
||||
self.on_file_remove()
|
||||
|
||||
def on_view_press(self, view, event):
|
||||
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_PRIMARY:
|
||||
target = view.get_path_at_pos(event.x, event.y)
|
||||
mask = not (event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK))
|
||||
if target and mask and view.get_selection().path_is_selected(target[0]):
|
||||
self._select_enabled = False
|
||||
|
||||
def on_view_release(self, view, event):
|
||||
""" Handles a mouse click (release) to view. """
|
||||
# Enable selection.
|
||||
self._select_enabled = True
|
||||
|
||||
def get_size_from_bytes(self, size):
|
||||
""" Simple convert function from bytes to other units like K, M or G. """
|
||||
try:
|
||||
b = float(size)
|
||||
except ValueError:
|
||||
return size
|
||||
else:
|
||||
kb, mb, gb = 1024.0, 1048576.0, 1073741824.0
|
||||
|
||||
if b < kb:
|
||||
return str(b)
|
||||
elif kb <= b < mb:
|
||||
return "{0:.1f} K".format(b / kb)
|
||||
elif mb <= b < gb:
|
||||
return "{0:.1f} M".format(b / mb)
|
||||
elif gb <= b:
|
||||
return "{0:.1f} G".format(b / gb)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
@@ -2,9 +2,8 @@ from contextlib import suppress
|
||||
from pathlib import Path
|
||||
|
||||
from app.commons import run_idle, log
|
||||
from app.eparser import get_bouquets, get_services
|
||||
from app.eparser import get_bouquets, get_services, BouquetsReader
|
||||
from app.eparser.ecommons import BqType, BqServiceType, Bouquet
|
||||
from app.eparser.enigma.bouquets import get_bouquet
|
||||
from app.eparser.neutrino.bouquets import parse_webtv, parse_bouquets as get_neutrino_bouquets
|
||||
from app.settings import SettingsType
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message
|
||||
@@ -67,7 +66,7 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa
|
||||
def get_enigma2_bouquet(path):
|
||||
path, sep, f_name = path.rpartition("userbouquet.")
|
||||
name, sep, suf = f_name.rpartition(".")
|
||||
bq = get_bouquet(path, name, suf)
|
||||
bq = BouquetsReader.get_bouquet(path, name, suf)
|
||||
bouquet = Bouquet(name=bq[0], type=BqType(suf).value, services=bq[1], locked=None, hidden=None)
|
||||
return bouquet
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkImage" id="remove_selection_image">
|
||||
<property name="visible">True</property>
|
||||
@@ -251,6 +251,9 @@ Author: Dmitriy Yefremov
|
||||
<row>
|
||||
<col id="0">eServiceUri</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">eServiceHLS</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkDialog" id="iptv_list_configuration_dialog">
|
||||
@@ -278,8 +281,8 @@ Author: Dmitriy Yefremov
|
||||
<property name="valign">center</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="close_config_list_button">
|
||||
<property name="label" translatable="yes">Close</property>
|
||||
<object class="GtkButton" id="cancel_config_list_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
@@ -379,10 +382,12 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="reset_to_default_lists_switch">
|
||||
<object class="GtkButton" id="reset_list_to_default_button">
|
||||
<property name="label" translatable="yes">Reset to default</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<signal name="state-set" handler="on_reset_to_default" object="start_values_frame" swapped="no"/>
|
||||
<property name="receives_default">True</property>
|
||||
<signal name="clicked" handler="on_reset_to_default" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -391,20 +396,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="reset_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_right">2</property>
|
||||
<property name="label" translatable="yes">Reset to default</property>
|
||||
<property name="xalign">1</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@@ -467,7 +458,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">5</property>
|
||||
<property name="text">0</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -510,7 +501,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">5</property>
|
||||
<property name="text">0</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -553,7 +544,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">5</property>
|
||||
<property name="text">0</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -596,7 +587,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">5</property>
|
||||
<property name="text">0</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -639,7 +630,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">5</property>
|
||||
<property name="text">1</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -773,7 +764,8 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="-6">close_config_list_button</action-widget>
|
||||
<action-widget response="-6">cancel_config_list_button</action-widget>
|
||||
<action-widget response="-10">list_configuration_apply_button</action-widget>
|
||||
</action-widgets>
|
||||
</object>
|
||||
<object class="GtkImage" id="yt_import_image">
|
||||
@@ -927,7 +919,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
@@ -950,7 +942,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
@@ -1050,7 +1042,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<property name="secondary_icon_tooltip_text" translatable="yes">Link to YouTube resource.</property>
|
||||
<signal name="changed" handler="on_url_changed" swapped="no"/>
|
||||
</object>
|
||||
@@ -1177,7 +1169,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">5</property>
|
||||
<property name="text">1</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1192,7 +1184,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">5</property>
|
||||
<property name="text">0</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1207,7 +1199,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">5</property>
|
||||
<property name="text">0</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1222,7 +1214,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">5</property>
|
||||
<property name="text">0</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1237,7 +1229,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">5</property>
|
||||
<property name="text">0</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
|
||||
334
app/ui/iptv.py
334
app/ui/iptv.py
@@ -1,21 +1,24 @@
|
||||
import concurrent.futures
|
||||
import os
|
||||
import re
|
||||
import urllib
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import urlparse, unquote, quote
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from gi.repository import GLib
|
||||
import requests
|
||||
from gi.repository import GLib, Gio, GdkPixbuf
|
||||
|
||||
from app.commons import run_idle, run_task
|
||||
from app.commons import run_idle, run_task, log
|
||||
from app.eparser.ecommons import BqServiceType, Service
|
||||
from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT
|
||||
from app.eparser.iptv import (NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT,
|
||||
parse_m3u)
|
||||
from app.settings import SettingsType
|
||||
from app.tools.yt import PlayListParser, YouTubeException, YouTube
|
||||
from .dialogs import Action, show_dialog, DialogType, get_dialogs_string, get_message
|
||||
from .main_helper import get_base_model, get_iptv_url, on_popup_menu
|
||||
from .uicommons import (Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON, Column, IS_GNOME_SESSION, KeyboardKey,
|
||||
get_yt_icon)
|
||||
from app.tools.yt import YouTubeException, YouTube
|
||||
from app.ui.dialogs import Action, show_dialog, DialogType, get_dialogs_string, get_message
|
||||
from app.ui.main_helper import get_base_model, get_iptv_url, on_popup_menu, get_picon_pixbuf
|
||||
from app.ui.uicommons import (Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON, Column, IS_GNOME_SESSION,
|
||||
KeyboardKey, get_yt_icon)
|
||||
|
||||
_DIGIT_ENTRY_NAME = "digit-entry"
|
||||
_ENIGMA2_REFERENCE = "{}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
|
||||
@@ -40,7 +43,9 @@ def get_stream_type(box):
|
||||
return StreamType.NONE_REC_1.value
|
||||
elif active == 3:
|
||||
return StreamType.NONE_REC_2.value
|
||||
return StreamType.E_SERVICE_URI.value
|
||||
elif active == 4:
|
||||
return StreamType.E_SERVICE_URI.value
|
||||
return StreamType.E_SERVICE_HLS.value
|
||||
|
||||
|
||||
class IptvDialog:
|
||||
@@ -161,6 +166,8 @@ class IptvDialog:
|
||||
self._stream_type_combobox.set_active(3)
|
||||
elif stream_type is StreamType.E_SERVICE_URI:
|
||||
self._stream_type_combobox.set_active(4)
|
||||
elif stream_type is StreamType.E_SERVICE_HLS:
|
||||
self._stream_type_combobox.set_active(5)
|
||||
except ValueError:
|
||||
self.show_info_message("Unknown stream type {}".format(s_type), Gtk.MessageType.ERROR)
|
||||
|
||||
@@ -200,7 +207,8 @@ class IptvDialog:
|
||||
def on_url_changed(self, entry):
|
||||
url_str = entry.get_text()
|
||||
url = urlparse(url_str)
|
||||
cond = all([url.scheme, url.netloc, url.path]) or self.get_type() == StreamType.E_SERVICE_URI.value
|
||||
e_types = (StreamType.E_SERVICE_URI.value, StreamType.E_SERVICE_HLS.value)
|
||||
cond = all([url.scheme, url.netloc, url.path]) or self.get_type() in e_types
|
||||
entry.set_name("GtkEntry" if cond else _DIGIT_ENTRY_NAME)
|
||||
|
||||
yt_id = YouTube.get_yt_id(url_str)
|
||||
@@ -251,7 +259,7 @@ class IptvDialog:
|
||||
yield True
|
||||
|
||||
def on_stream_type_changed(self, item):
|
||||
if self.get_type() == StreamType.E_SERVICE_URI.value:
|
||||
if self.get_type() in (StreamType.E_SERVICE_URI.value, StreamType.E_SERVICE_HLS.value):
|
||||
self.show_info_message("DreamOS only!", Gtk.MessageType.WARNING)
|
||||
self.update_reference_entry()
|
||||
|
||||
@@ -290,14 +298,14 @@ class IptvDialog:
|
||||
self._bouquet[self._paths[0][0]] = fav_id
|
||||
self._model.set(self._model.get_iter(self._paths), {Column.FAV_SERVICE: name, Column.FAV_ID: fav_id})
|
||||
else:
|
||||
aggr = [None] * 10
|
||||
s_type = BqServiceType.IPTV.name
|
||||
srv = (None, None, name, None, None, s_type, None, fav_id, *aggr[0:3])
|
||||
srv = (None, None, name, None, None, s_type, None, fav_id, None, None, None)
|
||||
itr = self._model.insert_after(self._model.get_iter(self._paths[0]),
|
||||
srv) if self._paths else self._model.insert(0, srv)
|
||||
self._model.set_value(itr, 1, IPTV_ICON)
|
||||
self._bouquet.insert(self._model.get_path(itr)[0], fav_id)
|
||||
self._services[fav_id] = Service(None, None, IPTV_ICON, name, *aggr[0:3], s_type, *aggr, fav_id, None)
|
||||
self._services[fav_id] = Service(None, None, IPTV_ICON, name, None, None, None, s_type, None,
|
||||
None, None, None, None, None, None, None, None, None, fav_id, None)
|
||||
|
||||
@run_idle
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
@@ -394,9 +402,10 @@ class SearchUnavailableDialog:
|
||||
self._dialog.destroy()
|
||||
|
||||
|
||||
class IptvListConfigurationDialog:
|
||||
class IptvListDialog:
|
||||
""" Base class for working with iptv lists. """
|
||||
|
||||
def __init__(self, transient, services, iptv_rows, bouquet, fav_model, s_type):
|
||||
def __init__(self, transient, s_type):
|
||||
handlers = {"on_apply": self.on_apply,
|
||||
"on_response": self.on_response,
|
||||
"on_stream_type_default_togged": self.on_stream_type_default_togged,
|
||||
@@ -410,10 +419,6 @@ class IptvListConfigurationDialog:
|
||||
"on_entry_changed": self.on_entry_changed,
|
||||
"on_info_bar_close": self.on_info_bar_close}
|
||||
|
||||
self._rows = iptv_rows
|
||||
self._services = services
|
||||
self._bouquet = bouquet
|
||||
self._fav_model = fav_model
|
||||
self._s_type = s_type
|
||||
|
||||
builder = Gtk.Builder()
|
||||
@@ -424,6 +429,8 @@ class IptvListConfigurationDialog:
|
||||
|
||||
self._dialog = builder.get_object("iptv_list_configuration_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._data_box = builder.get_object("iptv_list_data_box")
|
||||
self._start_values_grid = builder.get_object("start_values_grid")
|
||||
self._info_bar = builder.get_object("list_configuration_info_bar")
|
||||
self._reference_label = builder.get_object("reference_label")
|
||||
self._stream_type_check_button = builder.get_object("stream_type_default_check_button")
|
||||
@@ -438,22 +445,26 @@ class IptvListConfigurationDialog:
|
||||
self._list_tid_entry = builder.get_object("list_tid_entry")
|
||||
self._list_nid_entry = builder.get_object("list_nid_entry")
|
||||
self._list_namespace_entry = builder.get_object("list_namespace_entry")
|
||||
self._reset_to_default_switch = builder.get_object("reset_to_default_lists_switch")
|
||||
# style
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._apply_button = builder.get_object("list_configuration_apply_button")
|
||||
# Style
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._default_elems = (self._stream_type_check_button, self._type_check_button, self._sid_auto_check_button,
|
||||
self._tid_check_button, self._nid_check_button, self._namespace_check_button)
|
||||
self._digit_elems = (self._list_srv_type_entry, self._list_sid_entry, self._list_tid_entry,
|
||||
self._list_nid_entry, self._list_namespace_entry)
|
||||
for el in self._digit_elems:
|
||||
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
def show(self):
|
||||
self._dialog.run()
|
||||
|
||||
def on_response(self, dialog, response):
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
self._dialog.destroy()
|
||||
if response == Gtk.ResponseType.APPLY:
|
||||
return True
|
||||
|
||||
self._dialog.destroy()
|
||||
|
||||
def on_stream_type_changed(self, box):
|
||||
self.update_reference()
|
||||
@@ -489,19 +500,51 @@ class IptvListConfigurationDialog:
|
||||
self._list_namespace_entry.set_sensitive(not button.get_active())
|
||||
|
||||
@run_idle
|
||||
def on_reset_to_default(self, item, active):
|
||||
item.set_sensitive(not active)
|
||||
def on_reset_to_default(self, item):
|
||||
self._stream_type_combobox.set_active(1)
|
||||
self._list_srv_type_entry.set_text("1")
|
||||
for el in (self._list_sid_entry, self._list_nid_entry, self._list_tid_entry, self._list_namespace_entry):
|
||||
for el in self._digit_elems[1:]:
|
||||
el.set_text("0")
|
||||
for el in (self._stream_type_check_button, self._type_check_button, self._sid_auto_check_button,
|
||||
self._tid_check_button, self._nid_check_button, self._namespace_check_button):
|
||||
for el in self._default_elems:
|
||||
el.set_active(True)
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
def on_apply(self, item):
|
||||
pass
|
||||
|
||||
@run_idle
|
||||
def update_reference(self):
|
||||
if is_data_correct(self._digit_elems):
|
||||
stream_type = get_stream_type(self._stream_type_combobox)
|
||||
self._reference_label.set_text(
|
||||
_ENIGMA2_REFERENCE.format(stream_type, *[int(elem.get_text()) for elem in self._digit_elems]))
|
||||
|
||||
def on_entry_changed(self, entry):
|
||||
if _PATTERN.search(entry.get_text()):
|
||||
entry.set_name(_DIGIT_ENTRY_NAME)
|
||||
else:
|
||||
entry.set_name("GtkEntry")
|
||||
self.update_reference()
|
||||
|
||||
def is_default_values(self):
|
||||
return any(el.get_text() == "0" for el in self._digit_elems[2:])
|
||||
|
||||
def is_all_data_default(self):
|
||||
return all(el.get_active() for el in self._default_elems)
|
||||
|
||||
|
||||
class IptvListConfigurationDialog(IptvListDialog):
|
||||
|
||||
def __init__(self, transient, services, iptv_rows, bouquet, fav_model, s_type):
|
||||
super().__init__(transient, s_type)
|
||||
|
||||
self._rows = iptv_rows
|
||||
self._bouquet = bouquet
|
||||
self._fav_model = fav_model
|
||||
self._services = services
|
||||
|
||||
@run_idle
|
||||
def on_apply(self, item):
|
||||
if not is_data_correct(self._digit_elems):
|
||||
@@ -509,14 +552,13 @@ class IptvListConfigurationDialog:
|
||||
return
|
||||
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
reset = self._reset_to_default_switch.get_active()
|
||||
type_default = self._type_check_button.get_active()
|
||||
tid_default = self._tid_check_button.get_active()
|
||||
sid_auto = self._sid_auto_check_button.get_active()
|
||||
nid_default = self._nid_check_button.get_active()
|
||||
namespace_default = self._namespace_check_button.get_active()
|
||||
|
||||
stream_type = StreamType.NONE_TS.value if reset else get_stream_type(self._stream_type_combobox)
|
||||
stream_type = get_stream_type(self._stream_type_combobox)
|
||||
srv_type = "1" if type_default else self._list_srv_type_entry.get_text()
|
||||
tid = "0" if tid_default else "{:X}".format(int(self._list_tid_entry.get_text()))
|
||||
nid = "0" if nid_default else "{:X}".format(int(self._list_nid_entry.get_text()))
|
||||
@@ -527,7 +569,7 @@ class IptvListConfigurationDialog:
|
||||
data, sep, desc = fav_id.partition("http")
|
||||
data = data.split(":")
|
||||
|
||||
if reset:
|
||||
if self.is_all_data_default():
|
||||
data[2], data[3], data[4], data[5], data[6] = "10000"
|
||||
else:
|
||||
data[0], data[2], data[4], data[5], data[6] = stream_type, srv_type, tid, nid, namespace
|
||||
@@ -546,19 +588,208 @@ class IptvListConfigurationDialog:
|
||||
|
||||
self._info_bar.set_visible(True)
|
||||
|
||||
@run_idle
|
||||
def update_reference(self):
|
||||
if is_data_correct(self._digit_elems):
|
||||
stream_type = get_stream_type(self._stream_type_combobox)
|
||||
self._reference_label.set_text(
|
||||
_ENIGMA2_REFERENCE.format(stream_type, *[int(elem.get_text()) for elem in self._digit_elems]))
|
||||
|
||||
def on_entry_changed(self, entry):
|
||||
if _PATTERN.search(entry.get_text()):
|
||||
entry.set_name(_DIGIT_ENTRY_NAME)
|
||||
class M3uImportDialog(IptvListDialog):
|
||||
""" Import dialog for *.m3u* playlists. """
|
||||
|
||||
def __init__(self, transient, s_type, m3_path, app):
|
||||
super().__init__(transient, s_type)
|
||||
|
||||
self._app = app
|
||||
self._picons = app._picons
|
||||
self._pic_path = app._settings.picons_local_path
|
||||
self._services = None
|
||||
self._url_count = 0
|
||||
self._errors_count = 0
|
||||
self._max_count = 0
|
||||
self._is_download = False
|
||||
self._cancellable = Gio.Cancellable()
|
||||
self._dialog.set_title(get_message("Playlist import"))
|
||||
self._dialog.connect("delete-event", self.on_close)
|
||||
self._apply_button.set_label(get_message("Import"))
|
||||
# Progress
|
||||
self._progress_bar = Gtk.ProgressBar(visible=False, valign="center")
|
||||
self._spinner = Gtk.Spinner(active=False)
|
||||
self._info_label = Gtk.Label(visible=True, ellipsize="end", max_width_chars=30)
|
||||
load_label = Gtk.Label(label=get_message("Loading data..."))
|
||||
self._spinner.bind_property("active", self._spinner, "visible")
|
||||
self._spinner.bind_property("visible", load_label, "visible")
|
||||
self._spinner.bind_property("active", self._start_values_grid, "sensitive", 4)
|
||||
|
||||
progress_box = Gtk.HBox(visible=True, spacing=2)
|
||||
progress_box.add(self._progress_bar)
|
||||
progress_box.pack_end(self._spinner, False, False, 0)
|
||||
progress_box.pack_start(load_label, False, False, 0)
|
||||
# Picons
|
||||
self._picons_switch = Gtk.Switch(visible=True)
|
||||
self._picon_box = Gtk.HBox(visible=True, sensitive=False, spacing=2)
|
||||
self._picon_box.pack_end(self._picons_switch, False, False, 0)
|
||||
self._picon_box.pack_end(Gtk.Label(visible=True, label=get_message("Download picons")), False, False, 0)
|
||||
# Extra box
|
||||
extra_box = Gtk.HBox(visible=True, spacing=2, margin_bottom=5, margin_top=5)
|
||||
extra_box.set_center_widget(progress_box)
|
||||
extra_box.pack_start(self._info_label, False, False, 5)
|
||||
extra_box.pack_end(self._picon_box, True, True, 5)
|
||||
|
||||
frame = Gtk.Frame(visible=True)
|
||||
frame.add(extra_box)
|
||||
self._data_box.add(frame)
|
||||
|
||||
self.get_m3u(m3_path, s_type)
|
||||
|
||||
@run_task
|
||||
def get_m3u(self, path, s_type):
|
||||
try:
|
||||
GLib.idle_add(self._spinner.set_property, "active", True)
|
||||
self._services = parse_m3u(path, s_type)
|
||||
for s in self._services:
|
||||
if s.picon:
|
||||
GLib.idle_add(self._picon_box.set_sensitive, True)
|
||||
break
|
||||
finally:
|
||||
msg = "{} {}.".format(get_message("Streams detected:"), len(self._services) if self._services else 0)
|
||||
GLib.idle_add(self._info_label.set_text, msg)
|
||||
GLib.idle_add(self._spinner.set_property, "active", False)
|
||||
|
||||
def on_apply(self, item):
|
||||
if not is_data_correct(self._digit_elems):
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
return
|
||||
|
||||
picons = {}
|
||||
services = self._services
|
||||
|
||||
if not self.is_all_data_default():
|
||||
services = []
|
||||
params = [int(el.get_text()) for el in self._digit_elems]
|
||||
s_type = params[0]
|
||||
params = params[1:]
|
||||
stream_type = get_stream_type(self._stream_type_combobox)
|
||||
|
||||
for i, s in enumerate(self._services, start=params[0]):
|
||||
# Skipping markers.
|
||||
if not s.data_id:
|
||||
services.append(s)
|
||||
continue
|
||||
|
||||
params[0] = i
|
||||
picon_id = "{}_0_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png".format(stream_type, s_type, *params)
|
||||
fav_id = get_fav_id(s.data_id, s.service, self._s_type, params, stream_type, s_type)
|
||||
if s.picon:
|
||||
picons[s.picon] = picon_id
|
||||
|
||||
services.append(s._replace(picon=None, picon_id=picon_id, data_id=None, fav_id=fav_id))
|
||||
|
||||
if self._picons_switch.get_active():
|
||||
if self.is_default_values():
|
||||
show_dialog(DialogType.ERROR, self._dialog,
|
||||
"Set values for TID, NID and Namespace for correct naming of the picons!")
|
||||
return
|
||||
|
||||
self.download_picons(picons)
|
||||
else:
|
||||
entry.set_name("GtkEntry")
|
||||
self.update_reference()
|
||||
GLib.idle_add(self._info_bar.set_visible, True, priority=GLib.PRIORITY_LOW)
|
||||
|
||||
self._app.append_imported_services(services)
|
||||
|
||||
@run_task
|
||||
def download_picons(self, picons):
|
||||
self._is_download = True
|
||||
os.makedirs(os.path.dirname(self._pic_path), exist_ok=True)
|
||||
GLib.idle_add(self._apply_button.set_sensitive, False)
|
||||
GLib.idle_add(self._progress_bar.set_visible, True)
|
||||
|
||||
self._errors_count = 0
|
||||
self._url_count = len(picons)
|
||||
self._max_count = self._url_count
|
||||
self._cancellable.reset()
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
||||
futures = {executor.submit(self.download_picon, p, picons.get(p, None)): p for p in filter(None, picons)}
|
||||
done, not_done = concurrent.futures.wait(futures, timeout=0)
|
||||
while self._is_download and not_done:
|
||||
done, not_done = concurrent.futures.wait(not_done, timeout=5)
|
||||
|
||||
for future in not_done:
|
||||
future.cancel()
|
||||
concurrent.futures.wait(not_done)
|
||||
|
||||
self.update_progress(self._url_count)
|
||||
self.on_done()
|
||||
|
||||
def download_picon(self, url, pic_data):
|
||||
err_msg = "Picon download error: {} [{}]"
|
||||
timeout = (3, 5) # connect and read timeouts
|
||||
|
||||
req = requests.get(url, timeout=timeout)
|
||||
if req.status_code != 200:
|
||||
log(err_msg.format(url, req.reason))
|
||||
self.update_progress(1)
|
||||
else:
|
||||
self.on_picon_load_done(req.content, pic_data)
|
||||
|
||||
@run_idle
|
||||
def on_picon_load_done(self, data, user_data):
|
||||
try:
|
||||
self._info_label.set_text("Processing: {}".format(user_data))
|
||||
f = Gio.MemoryInputStream.new_from_data(data)
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, 220, 132, False, self._cancellable)
|
||||
path = "{}{}".format(self._pic_path, user_data)
|
||||
pixbuf.savev(path, "png", [], [])
|
||||
self._picons[user_data] = get_picon_pixbuf(path)
|
||||
except GLib.GError as e:
|
||||
self.update_progress(1)
|
||||
if e.code != Gio.IOErrorEnum.CANCELLED:
|
||||
log("Loading picon [{}] data error: {}".format(user_data, e))
|
||||
else:
|
||||
self.update_progress()
|
||||
|
||||
@run_idle
|
||||
def update_progress(self, error=0):
|
||||
self._errors_count += error
|
||||
self._url_count -= 1
|
||||
frac = 1 - self._url_count / self._max_count
|
||||
self._progress_bar.set_fraction(frac)
|
||||
|
||||
@run_idle
|
||||
def on_done(self):
|
||||
self._progress_bar.set_visible(False)
|
||||
self._progress_bar.set_fraction(0.0)
|
||||
self._apply_button.set_sensitive(True)
|
||||
self._info_label.set_text("Errors: {}.".format(self._errors_count))
|
||||
self._is_download = False
|
||||
|
||||
gen = self.update_fav_model()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def update_fav_model(self):
|
||||
services = self._app._services
|
||||
picons = self._app._picons
|
||||
model = self._app.fav_view.get_model()
|
||||
for r in model:
|
||||
s = services.get(r[Column.FAV_ID], None)
|
||||
if s:
|
||||
model.set_value(r.iter, Column.FAV_PICON, picons.get(s.picon_id, None))
|
||||
yield True
|
||||
self._info_bar.set_visible(True)
|
||||
yield True
|
||||
|
||||
def on_response(self, dialog, response):
|
||||
if response == Gtk.ResponseType.APPLY:
|
||||
return True
|
||||
|
||||
if response == Gtk.ResponseType.CANCEL and not self._is_download or not self.on_close():
|
||||
self._dialog.destroy()
|
||||
|
||||
def on_close(self, window=None, event=None):
|
||||
if self._is_download:
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
|
||||
self._is_download = False
|
||||
self._cancellable.cancel()
|
||||
return False
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class YtListImportDialog:
|
||||
@@ -666,7 +897,9 @@ class YtListImportDialog:
|
||||
def update_refs_list(self):
|
||||
if self._yt_list_id:
|
||||
try:
|
||||
self._yt_list_title, links = PlayListParser.get_yt_playlist(self._yt_list_id)
|
||||
if not self._yt:
|
||||
self._yt = YouTube.get_instance(self._settings)
|
||||
self._yt_list_title, links = self._yt.get_yt_playlist(self._yt_list_id, self._url_entry.get_text())
|
||||
except Exception as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
return
|
||||
@@ -687,13 +920,13 @@ class YtListImportDialog:
|
||||
|
||||
@run_idle
|
||||
def append_services(self, links):
|
||||
aggr = [None] * 9
|
||||
srvs = []
|
||||
|
||||
if self._yt_list_title:
|
||||
title = self._yt_list_title
|
||||
fav_id = MARKER_FORMAT.format(0, title, title)
|
||||
mk = Service(None, None, None, title, *aggr[0:3], BqServiceType.MARKER.name, *aggr, 0, fav_id, None)
|
||||
mk = Service(None, None, None, title, None, None, None, BqServiceType.MARKER.name, None,
|
||||
None, None, None, None, None, None, None, None, 0, fav_id, None)
|
||||
srvs.append(mk)
|
||||
|
||||
act = self._quality_model.get_value(self._quality_box.get_active_iter(), 0)
|
||||
@@ -703,7 +936,8 @@ class YtListImportDialog:
|
||||
continue
|
||||
ln = lnk.get(act) if act in lnk else lnk[sorted(lnk, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]
|
||||
fav_id = get_fav_id(ln, title, self._s_type)
|
||||
srv = Service(None, None, IPTV_ICON, title, *aggr[0:3], BqServiceType.IPTV.name, *aggr, None, fav_id, None)
|
||||
srv = Service(None, None, IPTV_ICON, title, None, None, None, BqServiceType.IPTV.name, None, None, None,
|
||||
None, None, None, None, None, None, None, fav_id, None)
|
||||
srvs.append(srv)
|
||||
self.appender(srvs)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,7 @@ def insert_marker(view, bouquets, selected_bouquet, services, parent_window, m_t
|
||||
marker = (None, None, text, None, None, s_type, None, fav_id, None, None, None)
|
||||
itr = model.insert_before(model.get_iter(paths[0]), marker) if paths else model.insert(0, marker)
|
||||
bouquets[selected_bouquet].insert(model.get_path(itr)[0], fav_id)
|
||||
services[fav_id] = Service(None, None, None, text, None, None, None, s_type, *[None] * 9, 0, fav_id, None)
|
||||
services[fav_id] = Service(None, None, None, text, None, None, None, s_type, None, None, None, None, None, None, None, None, None, 0, fav_id, None)
|
||||
|
||||
|
||||
# ***************** Movement *******************#
|
||||
@@ -280,7 +280,7 @@ def set_hide(services, model, paths):
|
||||
for path in paths:
|
||||
itr = model.get_iter(path)
|
||||
model.set_value(itr, col_num, None if hide else HIDE_ICON)
|
||||
flags = [*model.get_value(itr, 0).split(",")]
|
||||
flags = list(model.get_value(itr, 0).split(","))
|
||||
index, flag = None, None
|
||||
for i, fl in enumerate(flags):
|
||||
if fl.startswith("f:"):
|
||||
@@ -353,12 +353,12 @@ def scroll_to(index, view, paths=None):
|
||||
|
||||
# ***************** Picons *********************#
|
||||
|
||||
def update_picons_data(path, picons):
|
||||
def update_picons_data(path, picons, size=32):
|
||||
if not os.path.exists(path):
|
||||
return
|
||||
|
||||
for file in os.listdir(path):
|
||||
pf = get_picon_pixbuf(path + file)
|
||||
pf = get_picon_pixbuf(path + file, size)
|
||||
if pf:
|
||||
picons[file] = pf
|
||||
|
||||
@@ -530,7 +530,7 @@ def get_picon_pixbuf(path, size=32):
|
||||
|
||||
# ***************** Bouquets *********************#
|
||||
|
||||
def gen_bouquets(view, bq_view, transient, gen_type, tv_types, s_type, callback):
|
||||
def gen_bouquets(view, bq_view, transient, gen_type, s_type, callback):
|
||||
""" Auto-generate and append list of bouquets """
|
||||
fav_id_index = Column.SRV_FAV_ID
|
||||
index = Column.SRV_TYPE
|
||||
@@ -545,8 +545,6 @@ def gen_bouquets(view, bq_view, transient, gen_type, tv_types, s_type, callback)
|
||||
if not is_only_one_item_selected(paths, transient):
|
||||
return
|
||||
service = Service(*model[paths][:Column.SRV_TOOLTIP])
|
||||
if service.service_type not in tv_types:
|
||||
bq_type = BqType.RADIO.value
|
||||
append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model,
|
||||
[service.package if gen_type is BqGenType.PACKAGE else
|
||||
service.pos if gen_type is BqGenType.SAT else service.service_type], s_type)
|
||||
@@ -591,8 +589,10 @@ def get_bouquets_names(model):
|
||||
# ***************** Others *********************#
|
||||
|
||||
def update_entry_data(entry, dialog, settings):
|
||||
""" Updates value in text entry from chooser dialog """
|
||||
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, settings=settings)
|
||||
""" Updates value in text entry from chooser dialog. """
|
||||
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, settings=settings,
|
||||
action_type=Gtk.FileChooserAction.CREATE_FOLDER if settings.is_darwin else None,
|
||||
create_dir=True)
|
||||
if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
entry.set_text(response)
|
||||
return response
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -457,7 +457,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">picons_src_sort_model</property>
|
||||
<property name="headers_visible">False</property>
|
||||
<property name="tooltip_column">1</property>
|
||||
<property name="tooltip_column">0</property>
|
||||
<property name="activate_on_single_click">True</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="picons_src_view_popup_menu" swapped="no"/>
|
||||
<signal name="drag-data-get" handler="on_picons_view_drag_data_get" swapped="no"/>
|
||||
@@ -465,6 +465,7 @@ Author: Dmitriy Yefremov
|
||||
<signal name="drag-drop" handler="on_picons_src_view_drag_drop" swapped="no"/>
|
||||
<signal name="drag-end" handler="on_picons_src_view_drag_end" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_tree_view_key_press" swapped="no"/>
|
||||
<signal name="query-tooltip" handler="on_view_query_tooltip" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="picons_src_view_selection"/>
|
||||
</child>
|
||||
@@ -611,12 +612,13 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">picons_dst_sort_model</property>
|
||||
<property name="headers_visible">False</property>
|
||||
<property name="tooltip_column">1</property>
|
||||
<property name="tooltip_column">0</property>
|
||||
<property name="activate_on_single_click">True</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="picons_dest_view_popup_menu" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_picon_activated" swapped="no"/>
|
||||
<signal name="drag-data-get" handler="on_picons_view_drag_data_get" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_tree_view_key_press" swapped="no"/>
|
||||
<signal name="query-tooltip" handler="on_view_query_tooltip" swapped="no"/>
|
||||
<signal name="realize" handler="on_picons_dest_view_realize" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="picons_dest_view_selection"/>
|
||||
@@ -1022,7 +1024,9 @@ Author: Dmitriy Yefremov
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="name_cellrenderertext"/>
|
||||
<object class="GtkCellRendererText" id="name_cellrenderertext">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
@@ -1609,7 +1613,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="image">filter_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="toggled" handler="on_filter_toggled" swapped="no"/>
|
||||
<accelerator key="f" signal="clicked" modifiers="GDK_SHIFT_MASK | Primary"/>
|
||||
<accelerator key="f" signal="clicked" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -1625,7 +1629,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="receives_default">True</property>
|
||||
<property name="image">info_toggle_button_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<accelerator key="i" signal="clicked" modifiers="Primary"/>
|
||||
<accelerator key="i" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@@ -1656,7 +1660,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="image">cancel_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_cancel" swapped="no"/>
|
||||
<accelerator key="z" signal="clicked" modifiers="Primary"/>
|
||||
<accelerator key="z" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -1753,6 +1757,8 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="wrap_mode">word-char</property>
|
||||
<property name="left_margin">5</property>
|
||||
<property name="right_margin">5</property>
|
||||
<property name="overwrite">True</property>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse, unquote
|
||||
|
||||
from gi.repository import GLib, GdkPixbuf
|
||||
from gi.repository import GLib, GdkPixbuf, Gio
|
||||
|
||||
from app.commons import run_idle, run_task, run_with_delay
|
||||
from app.connections import upload_data, DownloadType, download_data, remove_picons
|
||||
from app.settings import SettingsType, Settings
|
||||
from app.tools.picons import PiconsParser, parse_providers, Provider, convert_to
|
||||
from app.tools.picons import PiconsParser, parse_providers, Provider, convert_to, download_picon
|
||||
from app.tools.satellites import SatellitesParser, SatelliteSource
|
||||
from .dialogs import show_dialog, DialogType, get_message
|
||||
from .main_helper import update_entry_data, append_text_to_tview, scroll_to, on_popup_menu, get_base_model, set_picon, \
|
||||
@@ -30,6 +29,7 @@ class PiconsDialog:
|
||||
self._POS_PATTERN = re.compile(r"^\d+\.\d+[EW]?$")
|
||||
self._current_process = None
|
||||
self._terminate = False
|
||||
self._is_downloading = False
|
||||
self._filter_binding = None
|
||||
self._services = None
|
||||
self._current_picon_info = None
|
||||
@@ -72,6 +72,7 @@ class PiconsDialog:
|
||||
"on_fiter_srcs_toggled": self.on_fiter_srcs_toggled,
|
||||
"on_filter_services_switch": self.on_filter_services_switch,
|
||||
"on_picon_activated": self.on_picon_activated,
|
||||
"on_view_query_tooltip": self.on_view_query_tooltip,
|
||||
"on_tree_view_key_press": self.on_tree_view_key_press,
|
||||
"on_popup_menu": on_popup_menu}
|
||||
|
||||
@@ -481,7 +482,7 @@ class PiconsDialog:
|
||||
try:
|
||||
for sat in sats:
|
||||
pos = sat[1]
|
||||
name, pos = "{} ({})".format(sat[0], pos), "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
|
||||
name = "{} ({})".format(sat[0], pos)
|
||||
|
||||
if not self._terminate and model:
|
||||
if pos in self._sat_positions:
|
||||
@@ -493,50 +494,29 @@ class PiconsDialog:
|
||||
model = view.get_model()
|
||||
self._url_entry.set_text(model.get(model.get_iter(path), 1)[0])
|
||||
|
||||
@run_idle
|
||||
def on_load_providers(self, item):
|
||||
self._expander.set_expanded(True)
|
||||
self.on_info_bar_close()
|
||||
self._cancel_button.show()
|
||||
url = self._url_entry.get_text()
|
||||
|
||||
try:
|
||||
exe = "{}wget".format("./" if GTK_PATH else "")
|
||||
self._current_process = subprocess.Popen([exe, "-pkP", self._TMP_DIR, url],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
except FileNotFoundError as e:
|
||||
self._cancel_button.hide()
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
|
||||
model = self._providers_view.get_model()
|
||||
model.clear()
|
||||
self.append_providers(url, model)
|
||||
model = self._providers_view.get_model()
|
||||
model.clear()
|
||||
self.get_providers(model)
|
||||
|
||||
@run_task
|
||||
def append_providers(self, url, model):
|
||||
self._current_process.wait()
|
||||
try:
|
||||
self._terminate = False
|
||||
providers = parse_providers(self._TMP_DIR + url[url.find("w"):])
|
||||
except FileNotFoundError:
|
||||
pass # NOP
|
||||
else:
|
||||
if providers:
|
||||
for p in providers:
|
||||
if self._terminate:
|
||||
return
|
||||
model.append((self.get_pixbuf(p[0]) if p[0] else TV_ICON, *p[1:]))
|
||||
self.update_receive_button_state()
|
||||
finally:
|
||||
GLib.idle_add(self._cancel_button.hide)
|
||||
self._terminate = False
|
||||
def get_providers(self, model):
|
||||
providers = parse_providers(self._url_entry.get_text())
|
||||
if providers:
|
||||
self.append_providers(providers, model)
|
||||
|
||||
def get_pixbuf(self, img_url):
|
||||
return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=self._TMP_DIR + "www.lyngsat.com/" + img_url,
|
||||
width=48, height=48, preserve_aspect_ratio=True)
|
||||
@run_idle
|
||||
def append_providers(self, providers, model):
|
||||
for p in providers:
|
||||
prv = p._replace(logo=self.get_pixbuf(p[0]) if p[0] else TV_ICON)
|
||||
model.append(prv)
|
||||
self.update_receive_button_state()
|
||||
|
||||
def get_pixbuf(self, img_data):
|
||||
if img_data:
|
||||
f = Gio.MemoryInputStream.new_from_data(img_data)
|
||||
return GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, 48, 32, True, None)
|
||||
|
||||
def on_receive(self, item):
|
||||
self._cancel_button.show()
|
||||
@@ -544,12 +524,12 @@ class PiconsDialog:
|
||||
|
||||
@run_task
|
||||
def start_download(self):
|
||||
if self._current_process.poll() is None:
|
||||
if self._is_downloading:
|
||||
self.show_dialog("The task is already running!", DialogType.ERROR)
|
||||
return
|
||||
|
||||
self._terminate = False
|
||||
self._expander.set_expanded(True)
|
||||
self._is_downloading = True
|
||||
GLib.idle_add(self._expander.set_expanded, True)
|
||||
|
||||
providers = self.get_selected_providers()
|
||||
for prv in providers:
|
||||
@@ -560,39 +540,50 @@ class PiconsDialog:
|
||||
return
|
||||
|
||||
try:
|
||||
for prv in providers:
|
||||
if self._terminate:
|
||||
return
|
||||
self.process_provider(Provider(*prv))
|
||||
picons_path = self._picons_dir_entry.get_text()
|
||||
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
|
||||
picons = []
|
||||
|
||||
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
|
||||
|
||||
import concurrent.futures
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
||||
# Getting links to picons.
|
||||
futures = {executor.submit(self.process_provider, Provider(*p), picons_path): p for p in providers}
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
if not self._is_downloading:
|
||||
executor.shutdown()
|
||||
return
|
||||
|
||||
pic = future.result()
|
||||
if pic:
|
||||
picons.extend(pic)
|
||||
|
||||
# Getting picon images.
|
||||
futures = {executor.submit(download_picon, pic[0], pic[1], self.append_output): pic for pic in picons}
|
||||
done, not_done = concurrent.futures.wait(futures, timeout=0)
|
||||
while self._is_downloading and not_done:
|
||||
done, not_done = concurrent.futures.wait(not_done, timeout=5)
|
||||
|
||||
for future in not_done:
|
||||
future.cancel()
|
||||
concurrent.futures.wait(not_done)
|
||||
|
||||
if not self._is_downloading:
|
||||
return
|
||||
|
||||
if not self._resize_no_radio_button.get_active():
|
||||
self.resize(self._picons_dir_entry.get_text())
|
||||
self.resize(picons_path)
|
||||
else:
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
finally:
|
||||
GLib.idle_add(self._cancel_button.hide)
|
||||
self._terminate = False
|
||||
self._is_downloading = False
|
||||
|
||||
def process_provider(self, prv):
|
||||
url = prv.url
|
||||
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
|
||||
exe = "{}wget".format("./" if GTK_PATH else "")
|
||||
self._current_process = subprocess.Popen([exe, "-pkP", self._TMP_DIR, url],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
|
||||
self._current_process.wait()
|
||||
path = self._TMP_DIR + (url[url.find("//") + 2:] if prv.single else self._BASE_URL + url[url.rfind("/") + 1:])
|
||||
PiconsParser.parse(path, self._picons_dir_entry.get_text(),
|
||||
self._TMP_DIR, prv, self._picon_ids, self.get_picons_format())
|
||||
|
||||
def write_to_buffer(self, fd, condition):
|
||||
if condition == GLib.IO_IN:
|
||||
char = fd.read(1)
|
||||
self.append_output(char)
|
||||
return char
|
||||
return False
|
||||
def process_provider(self, prv, picons_path):
|
||||
self.append_output("Getting links to picons for: {}.\n".format(prv.name))
|
||||
return PiconsParser.parse(prv, picons_path, self._picon_ids, self.get_picons_format())
|
||||
|
||||
@run_idle
|
||||
def append_output(self, char):
|
||||
@@ -618,7 +609,7 @@ class PiconsDialog:
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
|
||||
def on_cancel(self, item=None):
|
||||
if self.is_task_running() and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
if self._is_downloading and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return True
|
||||
|
||||
self.terminate_task()
|
||||
@@ -626,16 +617,15 @@ class PiconsDialog:
|
||||
@run_task
|
||||
def terminate_task(self):
|
||||
self._terminate = True
|
||||
|
||||
if self._current_process:
|
||||
self._current_process.terminate()
|
||||
self.show_info_message(get_message("The task is canceled!"), Gtk.MessageType.WARNING)
|
||||
self._is_downloading = False
|
||||
self.show_info_message(get_message("The task is canceled!"), Gtk.MessageType.WARNING)
|
||||
|
||||
def on_close(self, window, event):
|
||||
if self.on_cancel():
|
||||
return True
|
||||
|
||||
self._terminate = True
|
||||
self._is_downloading = False
|
||||
self.save_window_size(window)
|
||||
self.clean_data()
|
||||
self._app.update_picons()
|
||||
@@ -670,9 +660,10 @@ class PiconsDialog:
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._info_bar.set_visible(False)
|
||||
self._message_label.set_text(get_message(text))
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._info_bar.set_visible(True)
|
||||
|
||||
def on_picons_dir_open(self, entry, icon, event_button):
|
||||
update_entry_data(entry, self._dialog, settings=self._settings)
|
||||
@@ -768,6 +759,20 @@ class PiconsDialog:
|
||||
get_message("System"), srv.system, get_message("Freq"), srv.freq,
|
||||
ref)
|
||||
|
||||
def on_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
|
||||
dest = view.get_dest_row_at_pos(x, y)
|
||||
if not dest:
|
||||
return False
|
||||
|
||||
path, pos = dest
|
||||
model = view.get_model()
|
||||
row = model[path][:]
|
||||
tooltip.set_icon(get_picon_pixbuf(row[-1], size=self._settings.tooltip_logo_size))
|
||||
tooltip.set_text(row[1])
|
||||
view.set_tooltip_row(tooltip, path)
|
||||
|
||||
return True
|
||||
|
||||
def on_tree_view_key_press(self, view, event):
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
@@ -836,9 +841,6 @@ class PiconsDialog:
|
||||
|
||||
return picon_format
|
||||
|
||||
def is_task_running(self):
|
||||
return self._current_process and self._current_process.poll() is None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -3,34 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
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
|
||||
|
||||
-->
|
||||
<!--
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2021 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
|
||||
@@ -296,7 +269,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="image">popup_menu_add_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_satellite_add" swapped="no"/>
|
||||
<accelerator key="s" signal="activate" modifiers="Primary"/>
|
||||
<accelerator key="s" signal="activate" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@@ -307,7 +280,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="image">popup_menu_add_image_2</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_transponder_add" swapped="no"/>
|
||||
<accelerator key="t" signal="activate" modifiers="Primary"/>
|
||||
<accelerator key="t" signal="activate" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@@ -324,7 +297,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="activate" handler="on_edit" object="satellites_editor_tree_view" swapped="no"/>
|
||||
<accelerator key="e" signal="activate" modifiers="Primary"/>
|
||||
<accelerator key="e" signal="activate" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
@@ -577,11 +550,14 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="satellite_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min_width">20</property>
|
||||
<property name="min_width">250</property>
|
||||
<property name="title" translatable="yes">Satellite</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="satellite_cellrenderertext"/>
|
||||
<object class="GtkCellRendererText" id="satellite_cellrenderertext">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
@@ -594,6 +570,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="min_width">20</property>
|
||||
<property name="title" translatable="yes">Freq</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="frequency_cellrenderertext"/>
|
||||
<attributes>
|
||||
@@ -608,6 +585,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="min_width">20</property>
|
||||
<property name="title" translatable="yes">Rate</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="sat_rate_cellrenderertext"/>
|
||||
<attributes>
|
||||
@@ -622,6 +600,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="min_width">20</property>
|
||||
<property name="title" translatable="yes">Pol</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="sat_pol_cellrenderertext"/>
|
||||
<attributes>
|
||||
@@ -636,6 +615,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="min_width">20</property>
|
||||
<property name="title" translatable="yes">FEC</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="set_fec_cellrenderertext"/>
|
||||
<attributes>
|
||||
@@ -650,6 +630,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="min_width">20</property>
|
||||
<property name="title" translatable="yes">System</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="sys_cellrenderertext"/>
|
||||
<attributes>
|
||||
@@ -664,6 +645,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="min_width">20</property>
|
||||
<property name="title" translatable="yes">Mod</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="mod_cellrenderertext"/>
|
||||
<attributes>
|
||||
@@ -887,7 +869,6 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkEntry" id="sat_name_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_sensitive">False</property>
|
||||
@@ -1130,7 +1111,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_sensitive">False</property>
|
||||
@@ -1149,7 +1129,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="placeholder_text" translatable="yes">27500000</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
@@ -1305,7 +1284,6 @@ Author: Dmitriy Yefremov
|
||||
<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" translatable="yes">0 - 262142</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
@@ -1322,7 +1300,6 @@ Author: Dmitriy Yefremov
|
||||
<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" translatable="yes">0 - 255</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
@@ -1392,20 +1369,6 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkTreeModelSort" id="update_sat_list_model_sort">
|
||||
<property name="model">update_sat_list_model_filter</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="update_source_store">
|
||||
<columns>
|
||||
<!-- column-name source -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0">FlySat</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">LyngSat</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkListStore" id="update_service_store">
|
||||
<columns>
|
||||
<!-- column-name picon -->
|
||||
@@ -1490,7 +1453,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Cancel</property>
|
||||
<property name="valign">center</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"/>
|
||||
@@ -1528,7 +1490,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="image">sat_update_filter_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="toggled" handler="on_filter_toggled" swapped="no"/>
|
||||
<accelerator key="f" signal="clicked" modifiers="GDK_SHIFT_MASK | Primary"/>
|
||||
<accelerator key="f" signal="clicked" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -1546,7 +1508,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="image">sat_update_search_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="toggled" handler="on_find_toggled" swapped="no"/>
|
||||
<accelerator key="f" signal="clicked" modifiers="Primary"/>
|
||||
<accelerator key="f" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -1569,18 +1531,15 @@ Author: Dmitriy Yefremov
|
||||
<property name="valign">center</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="source_combo_box">
|
||||
<object class="GtkComboBoxText" id="source_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="model">update_source_store</property>
|
||||
<property name="active">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="source_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<items>
|
||||
<item id="FLYSAT" translatable="yes">FlySat</item>
|
||||
<item id="LYNGSAT" translatable="yes">LyngSat</item>
|
||||
<item id="KINGOFSAT" translatable="yes">KingOfSat</item>
|
||||
</items>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -1840,6 +1799,8 @@ Author: Dmitriy Yefremov
|
||||
<property name="model">update_sat_list_model_sort</property>
|
||||
<property name="search_column">0</property>
|
||||
<property name="activate_on_single_click">True</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="satellites_update_popup_menu" swapped="no"/>
|
||||
<signal name="select-all" handler="on_select_all" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection">
|
||||
<property name="mode">multiple</property>
|
||||
@@ -2134,6 +2095,8 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="left_margin">5</property>
|
||||
<property name="right_margin">5</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
@@ -185,7 +185,7 @@ class SatellitesDialog:
|
||||
model.set(edited_itr, {0: sat.name, 10: sat.flags, 11: sat.position})
|
||||
else:
|
||||
index = self.get_sat_position_index(sat.position, model)
|
||||
model.insert(None, index, [sat.name, *self._aggr, sat.flags, sat.position])
|
||||
model.insert(None, index, [sat.name, None, None, None, None, None, None, None, None, None, sat.flags, sat.position])
|
||||
scroll_to(index, view)
|
||||
|
||||
def on_transponder(self, transponder=None, edited_itr=None):
|
||||
@@ -210,7 +210,8 @@ class SatellitesDialog:
|
||||
4: tr.fec_inner, 5: tr.system, 6: tr.modulation,
|
||||
7: tr.pls_mode, 8: tr.pls_code, 9: tr.is_id})
|
||||
else:
|
||||
row = ["Transponder:", *tr, None, None]
|
||||
row = ["Transponder:", tr.frequency, tr.symbol_rate, tr.polarization, tr.fec_inner,
|
||||
tr.system, tr.modulation, tr.pls_mode, tr.pls_code, tr.is_id, None, None]
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
itr = model.get_iter(paths[0])
|
||||
view.expand_row(paths[0], 0)
|
||||
@@ -254,13 +255,22 @@ class SatellitesDialog:
|
||||
|
||||
return paths
|
||||
|
||||
@staticmethod
|
||||
def on_remove(view):
|
||||
@run_idle
|
||||
def on_remove(self, view):
|
||||
""" Removal of selected satellites and transponders.
|
||||
|
||||
The satellites are removed first! Then transponders.
|
||||
"""
|
||||
selection = view.get_selection()
|
||||
model, paths = selection.get_selected_rows()
|
||||
|
||||
for itr in [model.get_iter(path) for path in paths]:
|
||||
model.remove(itr)
|
||||
itrs = [model.get_iter(path) for path in paths]
|
||||
satellites = list(filter(model.iter_has_child, itrs))
|
||||
if len(satellites):
|
||||
# Removing selected satellites.
|
||||
list(map(model.remove, satellites))
|
||||
else:
|
||||
# Removing selected transponders.
|
||||
list(map(model.remove, itrs))
|
||||
|
||||
@run_idle
|
||||
def on_save(self, view):
|
||||
@@ -532,7 +542,13 @@ class UpdateDialog:
|
||||
|
||||
@run_task
|
||||
def get_sat_list(self, src, callback):
|
||||
sats = self._parser.get_satellites_list(SatelliteSource.FLYSAT if src == 0 else SatelliteSource.LYNGSAT)
|
||||
sat_src = SatelliteSource.FLYSAT
|
||||
if src == 1:
|
||||
sat_src = SatelliteSource.LYNGSAT
|
||||
elif src == 2:
|
||||
sat_src = SatelliteSource.KINGOFSAT
|
||||
|
||||
sats = self._parser.get_satellites_list(sat_src)
|
||||
if sats:
|
||||
callback(sats)
|
||||
self.is_download = False
|
||||
@@ -714,7 +730,8 @@ class SatellitesUpdateDialog(UpdateDialog):
|
||||
self._main_model.remove(ch.iter)
|
||||
|
||||
for tr in sat[3]:
|
||||
self._main_model.append(itr, ["Transponder:", *tr, None, None])
|
||||
self._main_model.append(itr, ["Transponder:", tr.frequency, tr.symbol_rate, tr.polarization, tr.fec_inner,
|
||||
tr.system, tr.modulation, tr.pls_mode, tr.pls_code, tr.is_id, None, None])
|
||||
|
||||
|
||||
class ServicesUpdateDialog(UpdateDialog):
|
||||
@@ -731,8 +748,8 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
self._services_parser = ServicesParser(source=SatelliteSource.LYNGSAT)
|
||||
|
||||
self._transponder_paned.set_visible(True)
|
||||
s_model = self._source_box.get_model()
|
||||
s_model.remove(s_model.get_iter_first())
|
||||
self._source_box.remove(0)
|
||||
self._source_box.remove(1)
|
||||
self._source_box.set_active(0)
|
||||
# Transponder view popup menu
|
||||
tr_popup_menu = Gtk.Menu()
|
||||
@@ -826,9 +843,10 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
appender.send("Consumed: {:0.0f}s, {} services received.".format(time.time() - start, len(services)))
|
||||
|
||||
try:
|
||||
from app.eparser.enigma.lamedb import get_services_lines, get_services_list
|
||||
from app.eparser.enigma.lamedb import LameDbReader
|
||||
# Used for double checking!
|
||||
srvs = get_services_list("".join(get_services_lines(services)))
|
||||
reader = LameDbReader(path=None)
|
||||
srvs = reader.get_services_list("".join(reader.get_services_lines(services)))
|
||||
except ValueError as e:
|
||||
log("ServicesUpdateDialog [on receive data] error: {}".format(e))
|
||||
else:
|
||||
@@ -940,9 +958,10 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
def append_satellite(model, sat):
|
||||
""" Common function for append satellite to the model """
|
||||
name, flags, pos, transponders = sat
|
||||
parent = model.append(None, [name, *(None,) * 9, flags, pos])
|
||||
for transponder in transponders:
|
||||
model.append(parent, ["Transponder:", *transponder, None, None])
|
||||
parent = model.append(None, [name, None, None, None, None, None, None, None, None, None, flags, pos])
|
||||
for tr in transponders:
|
||||
model.append(parent, ["Transponder:", tr.frequency, tr.symbol_rate, tr.polarization, tr.fec_inner, tr.system,
|
||||
tr.modulation, tr.pls_mode, tr.pls_code, tr.is_id, None, None])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,7 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkListStore" id="fec_list_store">
|
||||
<columns>
|
||||
<!-- column-name fec -->
|
||||
@@ -9,37 +40,37 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">1/2</col>
|
||||
<col id="0">1/2</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">2/3</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">3/4</col>
|
||||
<col id="0">3/4</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">5/6</col>
|
||||
<col id="0">5/6</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">7/8</col>
|
||||
<col id="0">7/8</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">8/9</col>
|
||||
<col id="0">8/9</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">3/5</col>
|
||||
<col id="0">3/5</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">4/5</col>
|
||||
<col id="0">4/5</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">6/7</col>
|
||||
<col id="0">6/7</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">9/10</col>
|
||||
<col id="0">9/10</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -63,13 +94,13 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Off</col>
|
||||
<col id="0">Off</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">On</col>
|
||||
<col id="0">On</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -80,19 +111,19 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">QPSK</col>
|
||||
<col id="0">QPSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">8PSK</col>
|
||||
<col id="0">8PSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">16APSK</col>
|
||||
<col id="0">16APSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">32APSK</col>
|
||||
<col id="0">32APSK</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -103,13 +134,13 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Off</col>
|
||||
<col id="0">Off</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">On</col>
|
||||
<col id="0">On</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -120,13 +151,13 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Root</col>
|
||||
<col id="0">Root</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Gold</col>
|
||||
<col id="0">Gold</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Combo</col>
|
||||
<col id="0">Combo</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -137,16 +168,16 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">H</col>
|
||||
<col id="0">H</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">V</col>
|
||||
<col id="0">V</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">R</col>
|
||||
<col id="0">R</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">L</col>
|
||||
<col id="0">L</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -157,21 +188,20 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">35%</col>
|
||||
<col id="0">35%</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">25%</col>
|
||||
<col id="0">25%</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">20%</col>
|
||||
<col id="0">20%</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkAdjustment" id="sat_pos_adjustment">
|
||||
<property name="lower">-180</property>
|
||||
<property name="upper">180</property>
|
||||
<property name="step_increment">0.10000000000000001</property>
|
||||
<property name="page_increment">10</property>
|
||||
@@ -223,10 +253,10 @@
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">DVB-S</col>
|
||||
<col id="0">DVB-S</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">DVB-S2</col>
|
||||
<col id="0">DVB-S2</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
@@ -265,6 +295,7 @@
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_cancel" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@@ -349,7 +380,6 @@
|
||||
<object class="GtkEntry" id="name_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
@@ -371,7 +401,7 @@
|
||||
<object class="GtkEntry" id="package_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
@@ -395,7 +425,7 @@
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">10</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
|
||||
<signal name="key-release-event" handler="update_reference" swapped="no"/>
|
||||
</object>
|
||||
@@ -440,7 +470,7 @@
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">7</property>
|
||||
<property name="max_width_chars">7</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
|
||||
<signal name="changed" handler="update_reference" swapped="no"/>
|
||||
</object>
|
||||
@@ -878,7 +908,7 @@
|
||||
<property name="tooltip_text">C:0000,C:a1b2,etc.</property>
|
||||
<property name="width_chars">15</property>
|
||||
<property name="max_width_chars">26</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<property name="placeholder_text" translatable="yes">C:0000,C:a1b2,etc.</property>
|
||||
<signal name="changed" handler="on_cas_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
@@ -981,7 +1011,7 @@
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">12</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1007,7 +1037,7 @@
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">12</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1086,7 +1116,7 @@
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">12</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
|
||||
<signal name="key-release-event" handler="update_reference" swapped="no"/>
|
||||
</object>
|
||||
@@ -1106,21 +1136,6 @@
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="sat_pos_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="input_purpose">number</property>
|
||||
<property name="adjustment">sat_pos_adjustment</property>
|
||||
<property name="digits">1</property>
|
||||
<property name="numeric">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="tid_label">
|
||||
<property name="visible">True</property>
|
||||
@@ -1139,7 +1154,7 @@
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">8</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
|
||||
<signal name="key-release-event" handler="update_reference" swapped="no"/>
|
||||
</object>
|
||||
@@ -1166,7 +1181,7 @@
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">8</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
|
||||
<signal name="key-release-event" handler="update_reference" swapped="no"/>
|
||||
</object>
|
||||
@@ -1175,6 +1190,50 @@
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="sat_pos_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">1</property>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="sat_pos_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="input_purpose">number</property>
|
||||
<property name="adjustment">sat_pos_adjustment</property>
|
||||
<property name="digits">1</property>
|
||||
<property name="numeric">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="pos_side_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="active">0</property>
|
||||
<items>
|
||||
<item id="E">E</item>
|
||||
<item id="W">W</item>
|
||||
</items>
|
||||
</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">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -1331,7 +1390,7 @@
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">8</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1357,7 +1416,7 @@
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">8</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1383,7 +1442,7 @@
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">8</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
|
||||
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -1509,7 +1568,7 @@
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImage" id="tr_edit_switch_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="visible">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="icon_name">document-edit-symbolic</property>
|
||||
@@ -1540,9 +1599,6 @@
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="-6">cancel_button</action-widget>
|
||||
</action-widgets>
|
||||
</object>
|
||||
<object class="GtkListStore" id="transponder_services_liststore">
|
||||
<columns>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from app.commons import run_idle
|
||||
from app.commons import run_idle, log
|
||||
from app.eparser import Service
|
||||
from app.eparser.ecommons import (MODULATION, Inversion, ROLL_OFF, Pilot, Flag, Pids, POLARIZATION, get_key_by_value,
|
||||
get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION,
|
||||
TrType, SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, T_FEC,
|
||||
HIERARCHY)
|
||||
HIERARCHY, A_MODULATION)
|
||||
from app.settings import SettingsType
|
||||
from .dialogs import show_dialog, DialogType, Action, get_dialogs_string
|
||||
from .main_helper import get_base_model
|
||||
@@ -43,7 +43,8 @@ class ServiceDetailsDialog:
|
||||
"update_reference": self.update_reference,
|
||||
"on_cas_entry_changed": self.on_cas_entry_changed,
|
||||
"on_digit_entry_changed": self.on_digit_entry_changed,
|
||||
"on_non_empty_entry_changed": self.on_non_empty_entry_changed}
|
||||
"on_non_empty_entry_changed": self.on_non_empty_entry_changed,
|
||||
"on_cancel": lambda item: self._dialog.destroy()}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
@@ -54,7 +55,7 @@ class ServiceDetailsDialog:
|
||||
self._dialog = builder.get_object("service_details_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._s_type = settings.setting_type
|
||||
self._tr_type = None
|
||||
self._tr_type = TrType.Satellite
|
||||
self._satellites_xml_path = settings.data_local_path + "satellites.xml"
|
||||
self._picons_dir_path = settings.picons_local_path
|
||||
self._services_view = srv_view
|
||||
@@ -120,6 +121,7 @@ class ServiceDetailsDialog:
|
||||
self._pids_grid = builder.get_object("pids_grid")
|
||||
# Transponder elements
|
||||
self._sat_pos_button = builder.get_object("sat_pos_button")
|
||||
self._pos_side_box = builder.get_object("pos_side_box")
|
||||
self._pol_combo_box = builder.get_object("pol_combo_box")
|
||||
self._fec_combo_box = builder.get_object("fec_combo_box")
|
||||
self._rate_lp_combo_box = builder.get_object("rate_lp_combo_box")
|
||||
@@ -137,7 +139,7 @@ class ServiceDetailsDialog:
|
||||
self._TRANSPONDER_ELEMENTS = (self._sat_pos_button, self._pol_combo_box, self._invertion_combo_box,
|
||||
self._sys_combo_box, self._freq_entry, self._transponder_id_entry,
|
||||
self._network_id_entry, self._namespace_entry, self._fec_combo_box,
|
||||
self._rate_entry, self._rate_lp_combo_box)
|
||||
self._rate_entry, self._rate_lp_combo_box, self._pos_side_box)
|
||||
|
||||
if self._action is Action.EDIT:
|
||||
self.update_data_elements()
|
||||
@@ -145,12 +147,7 @@ class ServiceDetailsDialog:
|
||||
self.init_default_data_elements()
|
||||
|
||||
def show(self):
|
||||
response = self._dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
pass
|
||||
self._dialog.destroy()
|
||||
|
||||
return response
|
||||
self._dialog.show()
|
||||
|
||||
@run_idle
|
||||
def init_default_data_elements(self):
|
||||
@@ -209,6 +206,8 @@ class ServiceDetailsDialog:
|
||||
self.update_ui_for_terrestrial()
|
||||
elif self._tr_type is TrType.Cable:
|
||||
self.update_ui_for_cable()
|
||||
elif self._tr_type is TrType.ATSC:
|
||||
self.update_ui_for_atsc()
|
||||
else:
|
||||
self.set_sat_positions(srv.pos)
|
||||
|
||||
@@ -310,6 +309,11 @@ class ServiceDetailsDialog:
|
||||
self.select_active_text(self._pls_mode_combo_box, HIERARCHY.get(tr_data[7]))
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[8]).name)
|
||||
self.select_active_text(self._sys_combo_box, T_SYSTEM.get(tr_data[9]))
|
||||
elif tr_type is TrType.ATSC:
|
||||
self._sys_combo_box.set_active(0)
|
||||
self.select_active_text(self._mod_combo_box, A_MODULATION.get(tr_data[2]))
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[1]).name)
|
||||
|
||||
# Should be called last to properly initialize the reference
|
||||
self._srv_type_entry.set_text(data[4])
|
||||
|
||||
@@ -336,7 +340,8 @@ class ServiceDetailsDialog:
|
||||
|
||||
def set_sat_positions(self, sat_pos):
|
||||
""" Sat positions initialisation """
|
||||
self._sat_pos_button.set_value(float(sat_pos))
|
||||
self._sat_pos_button.set_value(float(sat_pos[:-1]))
|
||||
self._pos_side_box.set_active_id(sat_pos[-1:])
|
||||
|
||||
def on_system_changed(self, box):
|
||||
if not self._tr_edit_switch.get_active():
|
||||
@@ -376,18 +381,18 @@ class ServiceDetailsDialog:
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
self.on_edit() if self._action is Action.EDIT else self.on_new()
|
||||
self._dialog.destroy()
|
||||
if self.on_edit() if self._action is Action.EDIT else self.on_new():
|
||||
self._dialog.destroy()
|
||||
|
||||
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!")
|
||||
return True
|
||||
|
||||
def on_edit(self):
|
||||
""" Edit current service. """
|
||||
fav_id, data_id = self.get_srv_data()
|
||||
# transponder
|
||||
# Transponder
|
||||
transponder = self._old_service.transponder
|
||||
if self._tr_edit_switch.get_active():
|
||||
try:
|
||||
@@ -397,17 +402,24 @@ class ServiceDetailsDialog:
|
||||
transponder = self.get_terrestrial_transponder_data()
|
||||
elif self._tr_type is TrType.Cable:
|
||||
transponder = self.get_cable_transponder_data()
|
||||
elif self._tr_type is TrType.ATSC:
|
||||
transponder = self.get_atsc_transponder_data()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
log("Edit service error: {}".format(e))
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Error getting transponder parameters!")
|
||||
else:
|
||||
if self._transponder_services_iters:
|
||||
self.update_transponder_services(transponder)
|
||||
# service
|
||||
self.update_transponder_services(transponder, self.get_sat_position())
|
||||
# Service
|
||||
service = self.get_service(fav_id, data_id, transponder)
|
||||
old_fav_id = self._old_service.fav_id
|
||||
if old_fav_id != fav_id:
|
||||
if fav_id in self._services:
|
||||
msg = "{}\n\n\t{}".format("A similar service is already in this list!", "Are you sure?")
|
||||
if show_dialog(DialogType.QUESTION, transient=self._dialog, text=msg) != Gtk.ResponseType.OK:
|
||||
return False
|
||||
self.update_bouquets(fav_id, old_fav_id)
|
||||
|
||||
self._services[fav_id] = service
|
||||
|
||||
if self._old_service.picon_id != service.picon_id:
|
||||
@@ -415,7 +427,7 @@ class ServiceDetailsDialog:
|
||||
|
||||
flags = service.flags_cas
|
||||
extra_data = {Column.SRV_TOOLTIP: None, Column.SRV_BACKGROUND: None}
|
||||
if flags:
|
||||
if self._s_type is SettingsType.ENIGMA_2 and flags:
|
||||
f_flags = list(filter(lambda x: x.startswith("f:"), flags.split(",")))
|
||||
if f_flags and Flag.is_new(int(f_flags[0][2:])):
|
||||
extra_data[Column.SRV_BACKGROUND] = self._new_color
|
||||
@@ -424,6 +436,7 @@ class ServiceDetailsDialog:
|
||||
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
|
||||
|
||||
def update_bouquets(self, fav_id, old_fav_id):
|
||||
self._services.pop(old_fav_id, None)
|
||||
@@ -491,7 +504,9 @@ class ServiceDetailsDialog:
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
return self.get_enigma2_flags()
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
return self._old_service.flags_cas
|
||||
flags = self._old_service.flags_cas.split(":")
|
||||
flags[1] = self.get_sat_position()
|
||||
return ":".join(flags)
|
||||
|
||||
def get_enigma2_flags(self):
|
||||
flags = ["p:{}".format(self._package_entry.get_text())]
|
||||
@@ -543,7 +558,9 @@ class ServiceDetailsDialog:
|
||||
return fav_id, data_id
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
fav_id = self._NEUTRINO_FAV_ID.format(tr_id, net_id, ssid)
|
||||
return fav_id, self._old_service.data_id
|
||||
data_id = self._old_service.data_id.split(":")
|
||||
data_id[1] = "{:x}".format(int(service_type))
|
||||
return fav_id, ":".join(data_id)
|
||||
|
||||
# ***************** Transponder ********************* #
|
||||
|
||||
@@ -551,27 +568,27 @@ class ServiceDetailsDialog:
|
||||
freq = self._freq_entry.get_text()
|
||||
fec = self._fec_combo_box.get_active_id()
|
||||
system = self._sys_combo_box.get_active_id()
|
||||
o_srv = self._old_service
|
||||
|
||||
if self._tr_type is TrType.Satellite or self._s_type is SettingsType.NEUTRINO_MP:
|
||||
freq = self._freq_entry.get_text()
|
||||
rate = self._rate_entry.get_text()
|
||||
pol = self._pol_combo_box.get_active_id()
|
||||
pos = str(round(self._sat_pos_button.get_value(), 1))
|
||||
pos = "{}{}".format(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 is TrType.Terrestrial:
|
||||
o_srv = self._old_service
|
||||
elif self._tr_type in (TrType.Terrestrial, TrType.ATSC):
|
||||
return freq, o_srv.rate, o_srv.pol, fec, system, o_srv.pos
|
||||
elif self._tr_type is TrType.Cable:
|
||||
o_srv = self._old_service
|
||||
return freq, self._rate_entry.get_text(), o_srv.pol, fec, o_srv.system, o_srv.pos
|
||||
|
||||
def get_satellite_transponder_data(self):
|
||||
sys = self._sys_combo_box.get_active_id()
|
||||
freq = self._freq_entry.get_text()
|
||||
rate = self._rate_entry.get_text()
|
||||
freq = "{}000".format(self._freq_entry.get_text())
|
||||
rate = "{}000".format(self._rate_entry.get_text())
|
||||
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 = str(round(self._sat_pos_button.get_value(), 1)).replace(".", "")
|
||||
sat_pos = self.get_sat_position()
|
||||
|
||||
inv = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
srv_sys = "0" # !!!
|
||||
|
||||
@@ -595,12 +612,17 @@ class ServiceDetailsDialog:
|
||||
srv_sys = None
|
||||
return self._NEUTRINO_TRANSPONDER_DATA.format(tr_id, on_id, freq, inv, rate, fec, pol, mod, srv_sys)
|
||||
|
||||
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)
|
||||
sat_pos = str(round(sat_pos, 1)).replace(".", "")
|
||||
return sat_pos
|
||||
|
||||
def get_terrestrial_transponder_data(self):
|
||||
tr_data = re.split("\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] = self._freq_entry.get_text()
|
||||
tr_data[1] = "{}000".format(self._freq_entry.get_text())
|
||||
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)
|
||||
@@ -610,28 +632,50 @@ class ServiceDetailsDialog:
|
||||
tr_data[8] = self.get_value_from_combobox_id(self._pls_mode_combo_box, HIERARCHY)
|
||||
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:]))
|
||||
|
||||
def get_cable_transponder_data(self):
|
||||
tr_data = re.split("\s|:", self._old_service.transponder)
|
||||
# frequency, symbol_rate, modulation, inversion, fec_inner, system;
|
||||
tr_data[1] = self._freq_entry.get_text()
|
||||
tr_data[2] = self._rate_entry.get_text()
|
||||
tr_data[1] = "{}000".format(self._freq_entry.get_text())
|
||||
tr_data[2] = "{}000".format(self._rate_entry.get_text())
|
||||
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:]))
|
||||
|
||||
def update_transponder_services(self, transponder):
|
||||
def get_atsc_transponder_data(self):
|
||||
tr_data = re.split("\s|:", self._old_service.transponder)
|
||||
# frequency, inversion, modulation, system
|
||||
tr_data[1] = "{}000".format(self._freq_entry.get_text())
|
||||
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:]))
|
||||
|
||||
def update_transponder_services(self, transponder, sat_pos):
|
||||
for itr in self._transponder_services_iters:
|
||||
srv = self._current_model[itr][:Column.SRV_TOOLTIP]
|
||||
srv = self._current_model[itr][:]
|
||||
srv[Column.SRV_FREQ], srv[Column.SRV_RATE], srv[Column.SRV_POL], srv[Column.SRV_FEC], srv[
|
||||
Column.SRV_SYSTEM], srv[Column.SRV_POS] = self.get_transponder_values()
|
||||
srv[Column.SRV_TRANSPONDER] = transponder
|
||||
srv = Service(*srv)
|
||||
self._services[srv.fav_id] = self._services.pop(srv.fav_id)._replace(transponder=transponder)
|
||||
self._current_model.set(itr, {i: v for i, v in enumerate(srv)})
|
||||
|
||||
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]))
|
||||
continue
|
||||
|
||||
if self._s_type is SettingsType.NEUTRINO_MP:
|
||||
flags = srv[Column.SRV_CAS_FLAGS].split(":")
|
||||
flags[1] = sat_pos
|
||||
srv[Column.SRV_CAS_FLAGS] = ":".join(flags)
|
||||
|
||||
self._services[fav_id] = Service(*srv[:Column.SRV_TOOLTIP])
|
||||
self._current_model.set_row(itr, srv)
|
||||
|
||||
# ***************** Others *********************#
|
||||
|
||||
@@ -660,16 +704,16 @@ class ServiceDetailsDialog:
|
||||
if active and self._action is Action.EDIT:
|
||||
self._transponder_services_iters = []
|
||||
response = TransponderServicesDialog(self._dialog,
|
||||
self._current_model,
|
||||
self._services_view,
|
||||
self._old_service.transponder,
|
||||
self._transponder_services_iters).show()
|
||||
if response == Gtk.ResponseType.CANCEL or response == -4:
|
||||
if response == Gtk.ResponseType.CANCEL or response == Gtk.ResponseType.DELETE_EVENT:
|
||||
switch.set_active(False)
|
||||
self._transponder_services_iters = None
|
||||
return
|
||||
|
||||
self.update_dvb_s2_elements(active and (self._sys_combo_box.get_active_id() == "DVB-S2"
|
||||
or self._old_service.transponder_type in "tc"))
|
||||
or self._old_service.transponder_type in "tca"))
|
||||
|
||||
for elem in self._TRANSPONDER_ELEMENTS:
|
||||
elem.set_sensitive(active)
|
||||
@@ -800,6 +844,18 @@ class ServiceDetailsDialog:
|
||||
self._transponder_id_entry.set_max_width_chars(8)
|
||||
self._network_id_entry.set_max_width_chars(8)
|
||||
|
||||
def update_ui_for_atsc(self):
|
||||
self.update_ui_for_cable()
|
||||
tr_grid = self._builder.get_object("tr_grid")
|
||||
tr_grid.remove_column(1)
|
||||
tr_grid.remove_column(1)
|
||||
# Init models
|
||||
fec_model, modulation_model, system_model = self.get_models_for_non_satellite()
|
||||
system_model.append((TrType.ATSC.name,))
|
||||
[modulation_model.append((v,)) for k, v in A_MODULATION.items()]
|
||||
# Extra
|
||||
self._namespace_entry.set_max_width_chars(25)
|
||||
|
||||
def get_transponder_grid_for_non_satellite(self):
|
||||
self._pids_grid.set_visible(False)
|
||||
tr_grid = self._builder.get_object("tr_grid")
|
||||
@@ -817,7 +873,7 @@ class ServiceDetailsDialog:
|
||||
|
||||
|
||||
class TransponderServicesDialog:
|
||||
def __init__(self, transient, model, transponder, tr_iters):
|
||||
def __init__(self, transient, services_view, transponder, tr_iters):
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
|
||||
@@ -825,15 +881,18 @@ class TransponderServicesDialog:
|
||||
self._dialog = builder.get_object("tr_services_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._srv_model = builder.get_object("transponder_services_liststore")
|
||||
self.append_services(model, transponder, tr_iters)
|
||||
self.append_services(services_view, transponder, tr_iters)
|
||||
builder.get_object("srv_list_dialog_info_bar").connect("response", lambda bar, resp: bar.hide())
|
||||
|
||||
def append_services(self, model, transponder, tr_iters):
|
||||
def append_services(self, view, transponder, tr_iters):
|
||||
model = view.get_model()
|
||||
filter_model = model.get_model()
|
||||
for row in model:
|
||||
if row[Column.SRV_TRANSPONDER] == transponder:
|
||||
self._srv_model.append((row[Column.SRV_SERVICE], row[Column.SRV_PACKAGE], row[Column.SRV_TYPE],
|
||||
row[Column.SRV_SSID], row[Column.SRV_FREQ], row[Column.SRV_POS]))
|
||||
tr_iters.append(model.get_iter(row.path))
|
||||
itr = model.get_iter(row.path)
|
||||
tr_iters.append(filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)))
|
||||
|
||||
def show(self):
|
||||
response = self._dialog.run()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,18 @@
|
||||
import os
|
||||
import re
|
||||
from enum import Enum
|
||||
|
||||
from app.commons import run_task, run_idle, log
|
||||
from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException
|
||||
from app.settings import SettingsType, Settings, PlayStreamsMode
|
||||
from app.settings import SettingsType, Settings, PlayStreamsMode, IS_WIN
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_message, get_chooser_dialog
|
||||
from .main_helper import update_entry_data, scroll_to, get_picon_pixbuf
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON, APP_FONT
|
||||
|
||||
|
||||
def show_settings_dialog(transient, options):
|
||||
return SettingsDialog(transient, options).show()
|
||||
|
||||
|
||||
class Property(Enum):
|
||||
FTP = "ftp"
|
||||
HTTP = "http"
|
||||
TELNET = "telnet"
|
||||
|
||||
|
||||
class SettingsDialog:
|
||||
_DIGIT_ENTRY_NAME = "digit-entry"
|
||||
_DIGIT_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
|
||||
@@ -50,7 +43,6 @@ class SettingsDialog:
|
||||
"on_profile_set_default": self.on_profile_set_default,
|
||||
"on_lang_changed": self.on_lang_changed,
|
||||
"on_main_settings_visible": self.on_main_settings_visible,
|
||||
"on_network_settings_visible": self.on_network_settings_visible,
|
||||
"on_http_use_ssl_toggled": self.on_http_use_ssl_toggled,
|
||||
"on_click_mode_togged": self.on_click_mode_togged,
|
||||
"on_play_mode_changed": self.on_play_mode_changed,
|
||||
@@ -58,10 +50,11 @@ class SettingsDialog:
|
||||
"on_apply_presets": self.on_apply_presets,
|
||||
"on_digit_entry_changed": self.on_digit_entry_changed,
|
||||
"on_view_popup_menu": self.on_view_popup_menu,
|
||||
"on_list_font_reset": self.on_list_font_reset,
|
||||
"on_theme_changed": self.on_theme_changed,
|
||||
"on_theme_add": self.on_theme_add,
|
||||
"on_theme_remove": self.on_theme_remove,
|
||||
"on_icon_theme_changed": self.on_icon_theme_changed,
|
||||
"on_appearance_changed": self.on_appearance_changed,
|
||||
"on_icon_theme_add": self.on_icon_theme_add,
|
||||
"on_icon_theme_remove": self.on_icon_theme_remove}
|
||||
|
||||
@@ -84,15 +77,13 @@ class SettingsDialog:
|
||||
self._port_field = builder.get_object("port_field")
|
||||
self._login_field = builder.get_object("login_field")
|
||||
self._password_field = builder.get_object("password_field")
|
||||
self._http_login_field = builder.get_object("http_login_field")
|
||||
self._http_password_field = builder.get_object("http_password_field")
|
||||
self._http_port_field = builder.get_object("http_port_field")
|
||||
self._http_use_ssl_check_button = builder.get_object("http_use_ssl_check_button")
|
||||
self._telnet_login_field = builder.get_object("telnet_login_field")
|
||||
self._telnet_password_field = builder.get_object("telnet_password_field")
|
||||
self._telnet_port_field = builder.get_object("telnet_port_field")
|
||||
self._telnet_timeout_spin_button = builder.get_object("telnet_timeout_spin_button")
|
||||
self._settings_stack = builder.get_object("settings_stack")
|
||||
# Test
|
||||
self._ftp_radio_button = builder.get_object("ftp_radio_button")
|
||||
self._http_radio_button = builder.get_object("http_radio_button")
|
||||
# Paths
|
||||
self._services_field = builder.get_object("services_field")
|
||||
self._user_bouquet_field = builder.get_object("user_bouquet_field")
|
||||
@@ -130,20 +121,26 @@ class SettingsDialog:
|
||||
self._edit_preset_switch.bind_property("active", builder.get_object("video_options_frame"), "sensitive")
|
||||
self._edit_preset_switch.bind_property("active", builder.get_object("audio_options_frame"), "sensitive")
|
||||
self._play_in_built_radio_button = builder.get_object("play_in_built_radio_button")
|
||||
self._play_in_vlc_radio_button = builder.get_object("play_in_vlc_radio_button")
|
||||
self._play_in_window_radio_button = builder.get_object("play_in_window_radio_button")
|
||||
self._get_m3u_radio_button = builder.get_object("get_m3u_radio_button")
|
||||
self._gst_lib_button = builder.get_object("gst_lib_button")
|
||||
self._vlc_lib_button = builder.get_object("vlc_lib_button")
|
||||
self._mpv_lib_button = builder.get_object("mpv_lib_button")
|
||||
# Program
|
||||
self._before_save_switch = builder.get_object("before_save_switch")
|
||||
self._before_downloading_switch = builder.get_object("before_downloading_switch")
|
||||
self._enable_experimental_box = builder.get_object("enable_experimental_box")
|
||||
self._colors_grid = builder.get_object("colors_grid")
|
||||
self._set_color_switch = builder.get_object("set_color_switch")
|
||||
self._new_color_button = builder.get_object("new_color_button")
|
||||
self._extra_color_button = builder.get_object("extra_color_button")
|
||||
self._load_on_startup_switch = builder.get_object("load_on_startup_switch")
|
||||
self._bouquet_hints_switch = builder.get_object("bouquet_hints_switch")
|
||||
self._services_hints_switch = builder.get_object("services_hints_switch")
|
||||
self._lang_combo_box = builder.get_object("lang_combo_box")
|
||||
# Appearance
|
||||
self._list_font_button = builder.get_object("list_font_button")
|
||||
self._picons_size_button = builder.get_object("picons_size_button")
|
||||
self._tooltip_logo_size_button = builder.get_object("tooltip_logo_size_button")
|
||||
self._colors_grid = builder.get_object("colors_grid")
|
||||
self._set_color_switch = builder.get_object("set_color_switch")
|
||||
self._new_color_button = builder.get_object("new_color_button")
|
||||
self._extra_color_button = builder.get_object("extra_color_button")
|
||||
# Extra
|
||||
self._support_http_api_switch = builder.get_object("support_http_api_switch")
|
||||
self._enable_yt_dl_switch = builder.get_object("enable_yt_dl_switch")
|
||||
@@ -185,25 +182,29 @@ class SettingsDialog:
|
||||
self.init_ui_elements(self._s_type)
|
||||
self.init_profiles()
|
||||
|
||||
if self._settings.is_darwin:
|
||||
# Appearance
|
||||
self._appearance_box = builder.get_object("appearance_box")
|
||||
self._appearance_box.set_visible(True)
|
||||
if IS_WIN:
|
||||
builder.get_object("streams_lib_frame").set_visible(False)
|
||||
# Themes
|
||||
enable_exp = self._settings.is_enable_experimental
|
||||
builder.get_object("style_frame").set_visible(enable_exp)
|
||||
builder.get_object("themes_support_frame").set_visible(enable_exp)
|
||||
self._layout_switch = builder.get_object("layout_switch")
|
||||
self._layout_switch.set_active(self._ext_settings.alternate_layout)
|
||||
self._theme_frame = builder.get_object("theme_frame")
|
||||
self._theme_frame.set_visible(enable_exp)
|
||||
self._theme_thumbnail_image = builder.get_object("theme_thumbnail_image")
|
||||
self._theme_combo_box = builder.get_object("theme_combo_box")
|
||||
self._icon_theme_combo_box = builder.get_object("icon_theme_combo_box")
|
||||
self._dark_mode_switch = builder.get_object("dark_mode_switch")
|
||||
self._themes_support_switch = builder.get_object("themes_support_switch")
|
||||
self._themes_support_switch.bind_property("active", builder.get_object("gtk_theme_frame"), "sensitive")
|
||||
self._themes_support_switch.bind_property("active", builder.get_object("icon_theme_frame"), "sensitive")
|
||||
self.init_appearance()
|
||||
self._themes_support_switch.bind_property("active", self._theme_frame, "sensitive")
|
||||
self.init_themes()
|
||||
|
||||
@run_idle
|
||||
def init_ui_elements(self, s_type):
|
||||
is_enigma_profile = s_type is SettingsType.ENIGMA_2
|
||||
self._neutrino_radio_button.set_active(s_type is SettingsType.NEUTRINO_MP)
|
||||
self.update_title()
|
||||
self._settings_stack.get_child_by_name(Property.HTTP.value).set_visible(is_enigma_profile)
|
||||
http_active = self._support_http_api_switch.get_active()
|
||||
self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active)
|
||||
self._lang_combo_box.set_active_id(self._ext_settings.language)
|
||||
@@ -259,12 +260,8 @@ class SettingsDialog:
|
||||
self._port_field.set_text(self._settings.port)
|
||||
self._login_field.set_text(self._settings.user)
|
||||
self._password_field.set_text(self._settings.password)
|
||||
self._http_login_field.set_text(self._settings.http_user)
|
||||
self._http_password_field.set_text(self._settings.http_password)
|
||||
self._http_port_field.set_text(self._settings.http_port)
|
||||
self._http_use_ssl_check_button.set_active(self._settings.http_use_ssl)
|
||||
self._telnet_login_field.set_text(self._settings.telnet_user)
|
||||
self._telnet_password_field.set_text(self._settings.telnet_password)
|
||||
self._telnet_port_field.set_text(self._settings.telnet_port)
|
||||
self._telnet_timeout_spin_button.set_value(self._settings.telnet_timeout)
|
||||
self._services_field.set_text(self._settings.services_path)
|
||||
@@ -280,6 +277,7 @@ class SettingsDialog:
|
||||
self._before_downloading_switch.set_active(self._settings.backup_before_downloading)
|
||||
self.set_fav_click_mode(self._settings.fav_click_mode)
|
||||
self.set_play_stream_mode(self._settings.play_streams_mode)
|
||||
self.set_stream_lib(self._settings.stream_lib)
|
||||
self._load_on_startup_switch.set_active(self._settings.load_last_config)
|
||||
self._bouquet_hints_switch.set_active(self._settings.show_bq_hints)
|
||||
self._services_hints_switch.set_active(self._settings.show_srv_hints)
|
||||
@@ -287,6 +285,9 @@ class SettingsDialog:
|
||||
self._transcoding_switch.set_active(self._settings.activate_transcoding)
|
||||
self._presets_combo_box.set_active_id(self._settings.active_preset)
|
||||
self.on_transcoding_preset_changed(self._presets_combo_box)
|
||||
self._picons_size_button.set_active_id(str(self._settings.list_picon_size))
|
||||
self._tooltip_logo_size_button.set_active_id(str(self._settings.tooltip_logo_size))
|
||||
self._list_font_button.set_font(self._settings.list_font)
|
||||
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self._enable_exp_switch.set_active(self._settings.is_enable_experimental)
|
||||
@@ -320,12 +321,8 @@ class SettingsDialog:
|
||||
self._settings.port = self._port_field.get_text()
|
||||
self._settings.user = self._login_field.get_text()
|
||||
self._settings.password = self._password_field.get_text()
|
||||
self._settings.http_user = self._http_login_field.get_text()
|
||||
self._settings.http_password = self._http_password_field.get_text()
|
||||
self._settings.http_port = self._http_port_field.get_text()
|
||||
self._settings.http_use_ssl = self._http_use_ssl_check_button.get_active()
|
||||
self._settings.telnet_user = self._telnet_login_field.get_text()
|
||||
self._settings.telnet_password = self._telnet_password_field.get_text()
|
||||
self._settings.telnet_port = self._telnet_port_field.get_text()
|
||||
self._settings.telnet_timeout = int(self._telnet_timeout_spin_button.get_value())
|
||||
self._settings.services_path = self._services_field.get_text()
|
||||
@@ -346,6 +343,7 @@ class SettingsDialog:
|
||||
self._ext_settings.backup_before_downloading = self._before_downloading_switch.get_active()
|
||||
self._ext_settings.fav_click_mode = self.get_fav_click_mode()
|
||||
self._ext_settings.play_streams_mode = self.get_play_stream_mode()
|
||||
self._ext_settings.stream_lib = self.get_stream_lib()
|
||||
self._ext_settings.language = self._lang_combo_box.get_active_id()
|
||||
self._ext_settings.load_last_config = self._load_on_startup_switch.get_active()
|
||||
self._ext_settings.show_bq_hints = self._bouquet_hints_switch.get_active()
|
||||
@@ -355,9 +353,13 @@ class SettingsDialog:
|
||||
self._ext_settings.records_path = self._record_data_dir_field.get_text()
|
||||
self._ext_settings.activate_transcoding = self._transcoding_switch.get_active()
|
||||
self._ext_settings.active_preset = self._presets_combo_box.get_active_id()
|
||||
self._ext_settings.list_picon_size = int(self._picons_size_button.get_active_id())
|
||||
self._ext_settings.tooltip_logo_size = int(self._tooltip_logo_size_button.get_active_id())
|
||||
self._ext_settings.list_font = self._list_font_button.get_font()
|
||||
|
||||
if self._ext_settings.is_darwin:
|
||||
if IS_WIN:
|
||||
self._ext_settings.dark_mode = self._dark_mode_switch.get_active()
|
||||
self._ext_settings.alternate_layout = self._layout_switch.get_active()
|
||||
self._ext_settings.is_themes_support = self._themes_support_switch.get_active()
|
||||
self._ext_settings.theme = self._theme_combo_box.get_active_id()
|
||||
self._ext_settings.icon_theme = self._icon_theme_combo_box.get_active_id()
|
||||
@@ -383,16 +385,15 @@ class SettingsDialog:
|
||||
if self._test_spinner.get_state() is Gtk.StateType.ACTIVE:
|
||||
return
|
||||
self.show_spinner(True)
|
||||
current_property = Property(self._settings_stack.get_visible_child_name())
|
||||
if current_property is Property.HTTP:
|
||||
self.test_http()
|
||||
elif current_property is Property.TELNET:
|
||||
self.test_telnet()
|
||||
elif current_property is Property.FTP:
|
||||
if self._ftp_radio_button.get_active():
|
||||
self.test_ftp()
|
||||
elif self._http_radio_button.get_active():
|
||||
self.test_http()
|
||||
else:
|
||||
self.test_telnet()
|
||||
|
||||
def test_http(self):
|
||||
user, password = self._http_login_field.get_text(), self._http_password_field.get_text()
|
||||
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()
|
||||
use_ssl = self._http_use_ssl_check_button.get_active()
|
||||
try:
|
||||
@@ -407,7 +408,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()
|
||||
user, password = self._telnet_login_field.get_text(), self._telnet_password_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)
|
||||
self.show_spinner(False)
|
||||
@@ -573,9 +574,6 @@ class SettingsDialog:
|
||||
self._apply_profile_button.set_visible(name == "profiles")
|
||||
self._apply_presets_button.set_visible(name == "streaming")
|
||||
|
||||
def on_network_settings_visible(self, stack, param):
|
||||
self._http_use_ssl_check_button.set_visible(Property(stack.get_visible_child_name()) is Property.HTTP)
|
||||
|
||||
def on_http_use_ssl_toggled(self, button):
|
||||
active = button.get_active()
|
||||
self._settings.http_use_ssl = active
|
||||
@@ -615,29 +613,48 @@ class SettingsDialog:
|
||||
return FavClickMode.DISABLED
|
||||
|
||||
def on_play_mode_changed(self, button):
|
||||
if self._main_stack.get_visible_child_name() != "streaming":
|
||||
if self._main_stack.get_visible_child_name() != "streaming" or not button.get_active():
|
||||
return
|
||||
|
||||
if self._settings.is_darwin:
|
||||
is_gst = self._gst_lib_button.get_active()
|
||||
self._play_in_built_radio_button.set_sensitive(is_gst)
|
||||
self._play_in_window_radio_button.set_active(not is_gst and self._play_in_built_radio_button.get_active())
|
||||
|
||||
if button.get_active():
|
||||
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
|
||||
|
||||
@run_idle
|
||||
def set_play_stream_mode(self, mode):
|
||||
self._play_in_built_radio_button.set_sensitive(not self._settings.is_darwin)
|
||||
self._play_in_built_radio_button.set_active(mode is PlayStreamsMode.BUILT_IN)
|
||||
self._play_in_vlc_radio_button.set_active(mode is PlayStreamsMode.VLC)
|
||||
self._play_in_window_radio_button.set_active(mode is PlayStreamsMode.WINDOW)
|
||||
self._get_m3u_radio_button.set_active(mode is PlayStreamsMode.M3U)
|
||||
|
||||
if self._settings.is_darwin and self._settings.stream_lib != "gst":
|
||||
self._play_in_built_radio_button.set_sensitive(False)
|
||||
|
||||
def get_play_stream_mode(self):
|
||||
if self._play_in_built_radio_button.get_active():
|
||||
return PlayStreamsMode.BUILT_IN
|
||||
if self._play_in_vlc_radio_button.get_active():
|
||||
return PlayStreamsMode.VLC
|
||||
if self._play_in_window_radio_button.get_active():
|
||||
return PlayStreamsMode.WINDOW
|
||||
if self._get_m3u_radio_button.get_active():
|
||||
return PlayStreamsMode.M3U
|
||||
|
||||
return self._settings.play_streams_mode
|
||||
|
||||
def set_stream_lib(self, mode):
|
||||
self._vlc_lib_button.set_active(mode == "vlc")
|
||||
self._gst_lib_button.set_active(mode == "gst")
|
||||
self._mpv_lib_button.set_active(mode == "mpv")
|
||||
|
||||
def get_stream_lib(self):
|
||||
if self._gst_lib_button.get_active():
|
||||
return "gst"
|
||||
elif self._vlc_lib_button.get_active():
|
||||
return "vlc"
|
||||
return "mpv"
|
||||
|
||||
def on_transcoding_preset_changed(self, button):
|
||||
presets = self._settings.transcoding_presets
|
||||
prs = presets.get(button.get_active_id())
|
||||
@@ -682,6 +699,11 @@ class SettingsDialog:
|
||||
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
|
||||
menu.popup(None, None, None, None, event.button, event.time)
|
||||
|
||||
def on_list_font_reset(self, button):
|
||||
self._list_font_button.set_font(APP_FONT)
|
||||
|
||||
# ******************* Themes *********************** #
|
||||
|
||||
def on_theme_changed(self, button):
|
||||
if self._main_stack.get_visible_child_name() != "appearance":
|
||||
return
|
||||
@@ -702,7 +724,7 @@ class SettingsDialog:
|
||||
Gtk.Settings().get_default().set_property("gtk-theme-name", "")
|
||||
self.remove_theme(self._theme_combo_box, self._ext_settings.themes_path)
|
||||
|
||||
def on_icon_theme_changed(self, button, state=False):
|
||||
def on_appearance_changed(self, button, state=False):
|
||||
if self._main_stack.get_visible_child_name() != "appearance":
|
||||
return
|
||||
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
|
||||
@@ -720,7 +742,7 @@ class SettingsDialog:
|
||||
response = get_chooser_dialog(self._dialog, self._settings, "Themes Archive [*.xz, *.zip]", ("*.xz", "*.zip"))
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
self._appearance_box.set_sensitive(False)
|
||||
self._theme_frame.set_sensitive(False)
|
||||
self.unpack_theme(response, path, button)
|
||||
|
||||
@run_task
|
||||
@@ -737,7 +759,6 @@ class SettingsDialog:
|
||||
log("Unpacking end.")
|
||||
finally:
|
||||
self.update_theme_button(button, dst)
|
||||
self._appearance_box.set_sensitive(True)
|
||||
|
||||
@run_idle
|
||||
def update_theme_button(self, button, dst):
|
||||
@@ -750,6 +771,7 @@ class SettingsDialog:
|
||||
button.append(theme, theme)
|
||||
button.set_active_id(theme)
|
||||
self.show_info_message("Done!", Gtk.MessageType.INFO)
|
||||
self._theme_frame.set_sensitive(True)
|
||||
|
||||
@run_idle
|
||||
def remove_theme(self, button, path):
|
||||
@@ -773,7 +795,7 @@ class SettingsDialog:
|
||||
button.set_active(0)
|
||||
|
||||
@run_idle
|
||||
def init_appearance(self):
|
||||
def init_themes(self):
|
||||
self._dark_mode_switch.set_active(self._ext_settings.dark_mode)
|
||||
t_support = self._ext_settings.is_themes_support
|
||||
self._themes_support_switch.set_active(t_support)
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
#digit-entry {
|
||||
border-color: Red;
|
||||
border-width: 0.15em;
|
||||
}
|
||||
|
||||
#status-bar-button {
|
||||
margin: 0.1em;
|
||||
padding: 1px;
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
paned > separator {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 2px 24px;
|
||||
}
|
||||
|
||||
.red-button {
|
||||
@@ -27,10 +33,6 @@
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
#textview-large {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.time-entry {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
@@ -39,8 +41,6 @@
|
||||
.group {}
|
||||
|
||||
.group :first-child {
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class TelnetDialog:
|
||||
try:
|
||||
GLib.idle_add(self._connect_button.set_visible, False)
|
||||
GLib.idle_add(self.on_info_bar_close)
|
||||
user, password = self._settings.telnet_user, self._settings.telnet_password
|
||||
user, password = self._settings.user, self._settings.password
|
||||
timeout = self._settings.telnet_timeout
|
||||
|
||||
self._tn = ExtTelnet(self.append_output,
|
||||
|
||||
@@ -5,7 +5,7 @@ import gi
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import log
|
||||
from app.settings import IS_DARWIN
|
||||
from app.settings import IS_WIN
|
||||
from app.connections import HttpAPI
|
||||
from app.tools.yt import YouTube
|
||||
from app.ui.iptv import get_yt_icon
|
||||
@@ -48,7 +48,7 @@ class LinksTransmitter:
|
||||
self._status_passive = None
|
||||
self._yt = YouTube.get_instance(settings)
|
||||
|
||||
if IS_DARWIN:
|
||||
if IS_WIN:
|
||||
self._tray = builder.get_object("status_icon")
|
||||
else:
|
||||
try:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import os
|
||||
from enum import Enum, IntEnum
|
||||
from functools import lru_cache
|
||||
from app.settings import Settings, SettingsException, IS_DARWIN
|
||||
|
||||
from app.settings import Settings, SettingsException, IS_WIN, SEP
|
||||
|
||||
import gi
|
||||
|
||||
@@ -10,15 +11,17 @@ gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
|
||||
# Setting mod mask for keyboard depending on platform
|
||||
MOD_MASK = Gdk.ModifierType.MOD2_MASK if IS_DARWIN else Gdk.ModifierType.CONTROL_MASK
|
||||
MOD_MASK = Gdk.ModifierType.CONTROL_MASK
|
||||
# Path to *.glade files
|
||||
UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "ui/"
|
||||
UI_PATH = "app{}ui{}".format(SEP, SEP)
|
||||
UI_RESOURCES_PATH = UI_PATH if os.path.exists(UI_PATH) else "ui{}".format(SEP)
|
||||
LANG_PATH = UI_RESOURCES_PATH + "lang"
|
||||
GTK_PATH = os.environ.get("GTK_PATH", None)
|
||||
NOTIFY_IS_INIT = False
|
||||
IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
|
||||
# Translation.
|
||||
TEXT_DOMAIN = "demon-editor"
|
||||
APP_FONT = None
|
||||
|
||||
try:
|
||||
settings = Settings.get_instance()
|
||||
@@ -26,43 +29,17 @@ except SettingsException:
|
||||
pass
|
||||
else:
|
||||
os.environ["LANGUAGE"] = settings.language
|
||||
|
||||
st = Gtk.Settings().get_default()
|
||||
APP_FONT = st.get_property("gtk-font-name")
|
||||
st.set_property("gtk-application-prefer-dark-theme", settings.dark_mode)
|
||||
|
||||
if settings.is_themes_support:
|
||||
st.set_property("gtk-theme-name", settings.theme)
|
||||
st.set_property("gtk-icon-theme-name", settings.icon_theme)
|
||||
else:
|
||||
style_provider = Gtk.CssProvider()
|
||||
s_path = "{}default_style.css".format(GTK_PATH + "/" + UI_RESOURCES_PATH if GTK_PATH else UI_RESOURCES_PATH)
|
||||
style_provider.load_from_path(s_path)
|
||||
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||
|
||||
if IS_DARWIN:
|
||||
import gettext
|
||||
|
||||
if GTK_PATH:
|
||||
LANG_PATH = GTK_PATH + "/share/locale"
|
||||
gettext.bindtextdomain(TEXT_DOMAIN, LANG_PATH)
|
||||
# For launching from the bundle.
|
||||
if os.getcwd() == "/" and GTK_PATH:
|
||||
os.chdir(GTK_PATH)
|
||||
else:
|
||||
import locale
|
||||
|
||||
locale.bindtextdomain(TEXT_DOMAIN, LANG_PATH)
|
||||
# Init notify
|
||||
try:
|
||||
gi.require_version("Notify", "0.7")
|
||||
from gi.repository import Notify
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
NOTIFY_IS_INIT = Notify.init("DemonEditor")
|
||||
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
theme.append_search_path(GTK_PATH + "/share/icons" if GTK_PATH else UI_RESOURCES_PATH + "icons")
|
||||
theme.append_search_path(GTK_PATH + "{}share{}icons".format(SEP, SEP) if GTK_PATH else UI_RESOURCES_PATH + "icons")
|
||||
|
||||
|
||||
def get_theme_icon(icon_theme, name, size):
|
||||
@@ -93,9 +70,11 @@ def get_yt_icon(icon_name, size=24):
|
||||
return default_theme.load_icon(icon_name, size, 0)
|
||||
|
||||
n_theme = Gtk.IconTheme.new()
|
||||
p_path = "{}usr{}share{}icons{}*".format(SEP, SEP, SEP, SEP)
|
||||
|
||||
import glob
|
||||
|
||||
for theme_name in map(os.path.basename, filter(os.path.isdir, glob.glob("/usr/share/icons/*"))):
|
||||
for theme_name in map(os.path.basename, filter(os.path.isdir, glob.glob(p_path))):
|
||||
theme.set_custom_theme(theme_name)
|
||||
if n_theme.has_icon(icon_name):
|
||||
return n_theme.load_icon(icon_name, size, 0)
|
||||
@@ -111,46 +90,38 @@ def show_notification(message, timeout=10000, urgency=1):
|
||||
@param timeout: milliseconds
|
||||
@param urgency: 0 - low, 1 - normal, 2 - critical
|
||||
"""
|
||||
if IS_DARWIN:
|
||||
# Since NSUserNotification has been deprecated, osascript will be used.
|
||||
os.system("""osascript -e 'display notification "{}" with title "DemonEditor"'""".format(message))
|
||||
elif NOTIFY_IS_INIT:
|
||||
notify = Notify.Notification.new("DemonEditor", message, "demon-editor")
|
||||
notify.set_urgency(urgency)
|
||||
notify.set_timeout(timeout)
|
||||
notify.show()
|
||||
pass
|
||||
|
||||
|
||||
class KeyboardKey(Enum):
|
||||
""" The raw(hardware) codes of the keyboard keys. """
|
||||
F = 3 if IS_DARWIN else 41
|
||||
E = 14 if IS_DARWIN else 26
|
||||
R = 15 if IS_DARWIN else 27
|
||||
T = 17 if IS_DARWIN else 28
|
||||
P = 35 if IS_DARWIN else 33
|
||||
S = 1 if IS_DARWIN else 39
|
||||
H = 4 if IS_DARWIN else 43
|
||||
L = 37 if IS_DARWIN else 46
|
||||
X = 7 if IS_DARWIN else 53
|
||||
C = 8 if IS_DARWIN else 54
|
||||
V = 9 if IS_DARWIN else 55
|
||||
W = 13 if IS_DARWIN else 25
|
||||
Z = 6 if IS_DARWIN else 52
|
||||
INSERT = -1 if IS_DARWIN else 118
|
||||
HOME = -1 if IS_DARWIN else 110
|
||||
END = -1 if IS_DARWIN else 115
|
||||
UP = 126 if IS_DARWIN else 111
|
||||
DOWN = 125 if IS_DARWIN else 116
|
||||
PAGE_UP = -1 if IS_DARWIN else 112
|
||||
PAGE_DOWN = -1 if IS_DARWIN else 117
|
||||
LEFT = 123 if IS_DARWIN else 113
|
||||
RIGHT = 123 if IS_DARWIN else 114
|
||||
F2 = 120 if IS_DARWIN else 68
|
||||
SPACE = 49 if IS_DARWIN else 65
|
||||
DELETE = 51 if IS_DARWIN else 119
|
||||
BACK_SPACE = 76 if IS_DARWIN else 22
|
||||
CTRL_L = 55 if IS_DARWIN else 37
|
||||
CTRL_R = 55 if IS_DARWIN else 105
|
||||
E = 69 if IS_WIN else 26
|
||||
R = 82 if IS_WIN else 27
|
||||
T = 84 if IS_WIN else 28
|
||||
P = 80 if IS_WIN else 33
|
||||
S = 83 if IS_WIN else 39
|
||||
F = 70 if IS_WIN else 41
|
||||
X = 88 if IS_WIN else 53
|
||||
C = 67 if IS_WIN else 54
|
||||
V = 86 if IS_WIN else 55
|
||||
W = 87 if IS_WIN else 25
|
||||
Z = 90 if IS_WIN else 52
|
||||
INSERT = 45 if IS_WIN else 118
|
||||
HOME = 36 if IS_WIN else 110
|
||||
END = 35 if IS_WIN else 115
|
||||
UP = 38 if IS_WIN else 111
|
||||
DOWN = 40 if IS_WIN else 116
|
||||
PAGE_UP = 33 if IS_WIN else 112
|
||||
PAGE_DOWN = 34 if IS_WIN else 117
|
||||
LEFT = 37 if IS_WIN else 113
|
||||
RIGHT = 39 if IS_WIN else 114
|
||||
F2 = 113 if IS_WIN else 68
|
||||
F7 = 118 if IS_WIN else 73
|
||||
SPACE = 32 if IS_WIN else 65
|
||||
DELETE = 46 if IS_WIN else 119
|
||||
BACK_SPACE = 8 if IS_WIN else 22
|
||||
CTRL_L = 17 if IS_WIN else 37
|
||||
CTRL_R = 163 if IS_WIN else 105
|
||||
# Laptop codes
|
||||
HOME_KP = 79
|
||||
END_KP = 87
|
||||
@@ -195,7 +166,7 @@ class BqGenType(Enum):
|
||||
|
||||
class Column(IntEnum):
|
||||
""" Column nums in the views """
|
||||
# main view
|
||||
# Main view
|
||||
SRV_CAS_FLAGS = 0
|
||||
SRV_STANDARD = 1
|
||||
SRV_CODED = 2
|
||||
@@ -218,7 +189,7 @@ class Column(IntEnum):
|
||||
SRV_TRANSPONDER = 19
|
||||
SRV_TOOLTIP = 20
|
||||
SRV_BACKGROUND = 21
|
||||
# fav view
|
||||
# FAV view
|
||||
FAV_NUM = 0
|
||||
FAV_CODED = 1
|
||||
FAV_SERVICE = 2
|
||||
@@ -230,11 +201,20 @@ class Column(IntEnum):
|
||||
FAV_PICON = 8
|
||||
FAV_TOOLTIP = 9
|
||||
FAV_BACKGROUND = 10
|
||||
# bouquets view
|
||||
# Bouquets view
|
||||
BQ_NAME = 0
|
||||
BQ_LOCKED = 1
|
||||
BQ_HIDDEN = 2
|
||||
BQ_TYPE = 3
|
||||
# Alternatives view
|
||||
ALT_NUM = 0
|
||||
ALT_PICON = 1
|
||||
ALT_SERVICE = 2
|
||||
ALT_TYPE = 3
|
||||
ALT_POS = 4
|
||||
ALT_FAV_ID = 5
|
||||
ALT_ID = 6
|
||||
ALT_ITER = 7
|
||||
|
||||
def __index__(self):
|
||||
""" Overridden to get the index in slices directly """
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 Dmitriy Yefremov
|
||||
# Copyright (C) 2018-2021 Dmitriy Yefremov
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
#
|
||||
@@ -909,13 +909,13 @@ msgid "Sample rate (Hz):"
|
||||
msgstr "Частата дыскр. (Гц):"
|
||||
|
||||
msgid "Play streams mode:"
|
||||
msgstr "Рэжым прайгравання струменяў:"
|
||||
msgstr "Рэжым прайгравання патокаў:"
|
||||
|
||||
msgid "Built-in player"
|
||||
msgstr "Убудаваны плэер"
|
||||
|
||||
msgid "VLC media player"
|
||||
msgstr "VLC медыяплэер"
|
||||
msgid "In a separate window"
|
||||
msgstr "У асобным акне"
|
||||
|
||||
msgid "Only get m3u file"
|
||||
msgstr "Атрымаць файл *.m3u"
|
||||
@@ -1159,3 +1159,72 @@ msgstr "Нд"
|
||||
|
||||
msgid "Set"
|
||||
msgstr "Усталяваць"
|
||||
|
||||
msgid "Services update"
|
||||
msgstr "Абнаўленне сэрвісаў"
|
||||
|
||||
msgid "Create folder"
|
||||
msgstr "Стварыць тэчку"
|
||||
|
||||
msgid "FTP client"
|
||||
msgstr "FTP-кліент"
|
||||
|
||||
msgid "The file size is too large!"
|
||||
msgstr "Памер файла занадта вялікі!"
|
||||
|
||||
msgid "Connect"
|
||||
msgstr "Злучэнне"
|
||||
|
||||
msgid "Disconnect"
|
||||
msgstr "Раз'яднаць"
|
||||
|
||||
msgid "Size"
|
||||
msgstr "Памер"
|
||||
|
||||
msgid "Date"
|
||||
msgstr "Дата"
|
||||
|
||||
msgid "Attr."
|
||||
msgstr "Атрыб."
|
||||
|
||||
msgid "Toggle display position"
|
||||
msgstr "Перамкнуць пазіцыю адлюстравання"
|
||||
|
||||
msgid "Alternatives"
|
||||
msgstr "Альтэрнатывы"
|
||||
|
||||
msgid "Add alternatives"
|
||||
msgstr "Дадаць альтэрнатывы"
|
||||
|
||||
msgid "DreamOS only!"
|
||||
msgstr "Толькі DreamOS!"
|
||||
|
||||
msgid "A similar service is already in this list!"
|
||||
msgstr "Падобны сэрвіс ужо ёсць у гэтым спісе!"
|
||||
|
||||
msgid "Play mode has been changed!\nRestart the program to apply the settings."
|
||||
msgstr "Зменены рэжым прайгравання!\nПеразапусціце праграму для ўжывання налад."
|
||||
|
||||
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
|
||||
msgstr "Усталюйце значэнні TID, NID і пр. імёнаў для слушнага наймення пiконаў!"
|
||||
|
||||
msgid "Streams detected:"
|
||||
msgstr "Выяўлена патокаў:"
|
||||
|
||||
msgid "Download picons"
|
||||
msgstr "Загрузіць пiконы"
|
||||
|
||||
msgid "Errors:"
|
||||
msgstr "Памылак:"
|
||||
|
||||
msgid "Use to play streams:"
|
||||
msgstr "Скарыстаць для прайгравання патокаў:"
|
||||
|
||||
msgid "Font in the lists:"
|
||||
msgstr "Шрыфт у спісах:"
|
||||
|
||||
msgid "Picons size in the lists:"
|
||||
msgstr "Памер пiконаў у спісах:"
|
||||
|
||||
msgid "Logo size in tooltips:"
|
||||
msgstr "Памер лагатыпа ва ўсплыўных падказках:"
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# Copyright (C) 2018-2020 Dmitriy Yefremov
|
||||
# Copyright (C) 2018-2021 Dmitriy Yefremov
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
# Charly, 2019.
|
||||
# Dmitriy Yefremov, 2020.
|
||||
# Dmitriy Yefremov, 2020-2021.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Last-Translator: Dmitriy Yefremov\n"
|
||||
"Last-Translator: Thomas Schmidt\n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "translator-credits"
|
||||
msgstr "Charly\nDmitriy Yefremov"
|
||||
msgstr "Charly\nDmitriy Yefremov\nThomas Schmidt"
|
||||
|
||||
# Main
|
||||
msgid "Service"
|
||||
@@ -22,7 +22,7 @@ msgid "Package"
|
||||
msgstr "Paket"
|
||||
|
||||
msgid "Type"
|
||||
msgstr "Model"
|
||||
msgstr "Typ"
|
||||
|
||||
msgid "Picon"
|
||||
msgstr "Picon"
|
||||
@@ -31,7 +31,7 @@ msgid "Freq"
|
||||
msgstr "Freq"
|
||||
|
||||
msgid "Rate"
|
||||
msgstr "Bewertung"
|
||||
msgstr "SR"
|
||||
|
||||
msgid "Pol"
|
||||
msgstr "Pol."
|
||||
@@ -666,16 +666,16 @@ msgid "Test connection"
|
||||
msgstr "Test Verbindung"
|
||||
|
||||
msgid "Double click on the service in the bouquet list:"
|
||||
msgstr "Doppelklicke auf das Service in der Bouquetliste:"
|
||||
msgstr "Doppelklick auf das Service in der Bouquet-Liste:"
|
||||
|
||||
msgid "Zap"
|
||||
msgstr "Zap"
|
||||
|
||||
msgid "Play stream"
|
||||
msgstr "Play Stream"
|
||||
msgstr "Stream abspielen"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Ausgeschaltet"
|
||||
msgstr "Deaktiviert"
|
||||
|
||||
msgid "Enable lamedb ver. 5 support"
|
||||
msgstr "Lamedb ver. 5 Unterstützung aktivieren"
|
||||
@@ -693,7 +693,7 @@ msgid "Play IPTV or other stream in the program(Ctrl + P)"
|
||||
msgstr "Wiedergabe von IPTV oder anderen Streams im Programm(Strg + P)"
|
||||
|
||||
msgid "Export to m3u"
|
||||
msgstr "Export nach m3u"
|
||||
msgstr "Exportieren nach m3u"
|
||||
|
||||
msgid "EPG configuration"
|
||||
msgstr "EPG Konfiguration"
|
||||
@@ -720,13 +720,13 @@ msgid "Url to *.xml.gz file:"
|
||||
msgstr "Url zur *.xml.gz Datei:"
|
||||
|
||||
msgid "Enable filtering"
|
||||
msgstr "Filterung einschalten"
|
||||
msgstr "Filter aktivieren"
|
||||
|
||||
msgid "Filter by presence in the epg.dat file."
|
||||
msgstr "Filtern nach dem Vorhandensein in der epg.dat Datei."
|
||||
|
||||
msgid "Paths to the epg.dat file:"
|
||||
msgstr "Pfade zur epg.dat Datei:"
|
||||
msgstr "Pfad zur epg.dat Datei:"
|
||||
|
||||
msgid "Local path:"
|
||||
msgstr "Local path:"
|
||||
@@ -750,7 +750,7 @@ msgid "Unsupported file type:"
|
||||
msgstr "Nicht unterstützter Dateityp:"
|
||||
|
||||
msgid "Unpacking data error."
|
||||
msgstr "Fehler beim Entpacken von Daten."
|
||||
msgstr "Fehler beim Entpacken der Daten."
|
||||
|
||||
msgid "XML parsing error:"
|
||||
msgstr "XML Parsing-Fehler:"
|
||||
@@ -765,7 +765,7 @@ msgid "Use HTTP"
|
||||
msgstr "HTTP verwenden"
|
||||
|
||||
msgid "Close playback"
|
||||
msgstr "Wiedergabe schliessen"
|
||||
msgstr "Wiedergabe schließen"
|
||||
|
||||
msgid "Import YouTube playlist"
|
||||
msgstr "YouTube-Wiedergabeliste importieren"
|
||||
@@ -774,8 +774,8 @@ msgid ""
|
||||
"Found a link to the YouTube resource!\n"
|
||||
"Try to get a direct link to the video?"
|
||||
msgstr ""
|
||||
"Ich habe einen Link zur YouTube-Ressource gefunden!\n"
|
||||
"Versuchen einen direkten Link zum Video zu bekommen?"
|
||||
"Link zur YouTube-Ressource gefunden!\n"
|
||||
"Soll versucht werden, einen Direkt-Link zum Video zu erzeugen?"
|
||||
|
||||
msgid "Playlist import"
|
||||
msgstr "Playlist-Import"
|
||||
@@ -784,7 +784,7 @@ msgid "Getting link error:"
|
||||
msgstr "Link-Fehler erhalten:"
|
||||
|
||||
msgid "Extra"
|
||||
msgstr "Extra"
|
||||
msgstr "Extras"
|
||||
|
||||
msgid "Apply profile settings"
|
||||
msgstr "Profileinstellungen anwenden"
|
||||
@@ -793,7 +793,7 @@ msgid "Settings type:"
|
||||
msgstr "Art der Einstellungen:"
|
||||
|
||||
msgid "Set default"
|
||||
msgstr "Standard setzen"
|
||||
msgstr "Standard wiederherstellen"
|
||||
|
||||
msgid "Language:"
|
||||
msgstr "Sprache:"
|
||||
@@ -805,16 +805,16 @@ msgid "Enable direct playback bar"
|
||||
msgstr "Aktivieren der direkten Wiedergabeleiste"
|
||||
|
||||
msgid "Enables direct sending and playback of media links on the receiver"
|
||||
msgstr "Ermöglicht das direkte Senden und Abspielen von Medienlinks auf dem Box"
|
||||
msgstr "Ermöglicht das direkte Senden und Abspielen von Medienlinks auf dem Receiver"
|
||||
|
||||
msgid "Watch the channel in the program"
|
||||
msgstr "Gucken den Kanal im Programm an"
|
||||
msgstr "Kanal im Programm ansehen"
|
||||
|
||||
msgid "Zap and Play"
|
||||
msgstr "Zap und Abspielen"
|
||||
|
||||
msgid "Drag or paste the link here"
|
||||
msgstr "Ziehe den Link hierher oder füge ihn ein"
|
||||
msgstr "Link hineinziehen oder einfügen"
|
||||
|
||||
msgid "Remove added links in the playlist"
|
||||
msgstr "Hinzugefügte Links in der Wiedergabeliste entfernen"
|
||||
@@ -832,13 +832,13 @@ msgid "Reset"
|
||||
msgstr "Reset"
|
||||
|
||||
msgid "File"
|
||||
msgstr "Ablage"
|
||||
msgstr "Datei"
|
||||
|
||||
msgid "Picons manager"
|
||||
msgstr "Picons-Manager"
|
||||
|
||||
msgid "Explorer"
|
||||
msgstr ""
|
||||
msgstr "Explorer"
|
||||
|
||||
msgid "Satellite url:"
|
||||
msgstr "Satellit URL:"
|
||||
@@ -916,40 +916,40 @@ msgid "Height (px):"
|
||||
msgstr "Höhe (px):"
|
||||
|
||||
msgid "Channels:"
|
||||
msgstr "Kanälen:"
|
||||
msgstr "Kanäle:"
|
||||
|
||||
msgid "Sample rate (Hz):"
|
||||
msgstr "Samplerate (Hz):"
|
||||
msgstr "Sample-Rate (Hz):"
|
||||
|
||||
msgid "Play streams mode:"
|
||||
msgstr "Streams Abspielen-Modus:"
|
||||
msgstr "Stream-Abspielmodus:"
|
||||
|
||||
msgid "Built-in player"
|
||||
msgstr "Integrierter Player"
|
||||
|
||||
msgid "VLC media player"
|
||||
msgstr "VLC Media Player"
|
||||
msgid "In a separate window"
|
||||
msgstr "In einem separaten Fenster"
|
||||
|
||||
msgid "Only get m3u file"
|
||||
msgstr "Nur m3u-Datei erhalten"
|
||||
|
||||
msgid "Save and restart the program to apply the settings."
|
||||
msgstr "Speicher und starte das Programm neu, um die Einstellungen zu übernehmen."
|
||||
msgstr "Änderungen speichern und anschließend das Programm neustarten, um die Einstellungen zu übernehmen."
|
||||
|
||||
msgid "Some images may have problems displaying the favorites list!"
|
||||
msgstr "Einige Images können Probleme mit der Anzeige der Favoritenliste haben!"
|
||||
msgstr "Einige Bilder können Probleme in der Anzeige der Favoritenliste haben!"
|
||||
|
||||
msgid "Operates in standby mode or current active transponder!"
|
||||
msgstr "Arbeitet im Standby-Modus oder auf dem aktuell aktiven Transponder!"
|
||||
msgstr "Arbeitet im Standby-Modus oder der Transponder ist bereits in Verwendung!"
|
||||
|
||||
msgid "No connection to the receiver!"
|
||||
msgstr "Keine Verbindung zum Box!"
|
||||
msgstr "Keine Verbindung zum Receiver!"
|
||||
|
||||
msgid "Signal level"
|
||||
msgstr "Signalpegel"
|
||||
|
||||
msgid "Receiver info"
|
||||
msgstr "Box-Info"
|
||||
msgstr "Receiver-Info"
|
||||
|
||||
msgid "A profile with that name exists!"
|
||||
msgstr "Ein Profil mit diesem Namen existiert!"
|
||||
@@ -958,10 +958,10 @@ msgid "Show short info as hints in the main services list"
|
||||
msgstr "Kurzinfos als Tooltips in der Hauptliste anzeigen"
|
||||
|
||||
msgid "Show detailed info as hints in the bouquet list"
|
||||
msgstr "Detaillierteinfos als Tooltips in der Bouquetliste anzeigen"
|
||||
msgstr "Detaillierte Informationen als Tooltips in der Bouquetliste anzeigen"
|
||||
|
||||
msgid "Enable alternate bouquet file naming"
|
||||
msgstr "Aktivieren der Alternativerbenennung für Bouquet-Dateien"
|
||||
msgstr "Aktivieren der alternativen Benennung für Bouquet-Dateien"
|
||||
|
||||
msgid "Allows you to name bouquet files using their names."
|
||||
msgstr "Ermöglicht Bouquet-Dateien mit ihren Namen zu benennen."
|
||||
@@ -970,7 +970,7 @@ msgid "Appearance"
|
||||
msgstr "Aussehen"
|
||||
|
||||
msgid "Enable Themes support"
|
||||
msgstr "Unterstützung von Themen aktivieren"
|
||||
msgstr "Theme Unterstützung aktivieren"
|
||||
|
||||
msgid "Gtk3 Theme:"
|
||||
msgstr "Gtk3-Theme:"
|
||||
@@ -979,7 +979,7 @@ msgid "Icon Theme:"
|
||||
msgstr "Icon-Theme:"
|
||||
|
||||
msgid "Gtk3 Themes and Icons:"
|
||||
msgstr "Gtk3 Themes and Icons:"
|
||||
msgstr "Gtk3 Themes und Icons:"
|
||||
|
||||
msgid "Deleting data..."
|
||||
msgstr "Daten löschen..."
|
||||
@@ -988,7 +988,7 @@ msgid "Download from the receiver"
|
||||
msgstr "Downloaden vom Receiver"
|
||||
|
||||
msgid "Remove all picons from the receiver"
|
||||
msgstr "Alle Picons aus dem Receiver entfernen"
|
||||
msgstr "Alle Picons vom dem Receiver entfernen"
|
||||
|
||||
msgid "Service reference"
|
||||
msgstr "Kanalreferenz"
|
||||
@@ -1021,7 +1021,7 @@ msgid "Are you sure you want to change the order\n\t of services in this bouquet
|
||||
msgstr "Bist du sicher, dass du die Reihenfolge der Dienstleistungen\n\t in diesem Bouquet ändern willst?"
|
||||
|
||||
msgid "Remove from the receiver"
|
||||
msgstr "Aus dem Receiver entfernen"
|
||||
msgstr "Vom Receiver entfernen"
|
||||
|
||||
msgid "Screenshot"
|
||||
msgstr "Screenshot"
|
||||
@@ -1039,7 +1039,7 @@ msgid "Can't Playback!"
|
||||
msgstr "Kann nicht abgespielt werden!"
|
||||
|
||||
msgid "Enable Dark Mode"
|
||||
msgstr "Dunkelmodus aktivieren"
|
||||
msgstr "Dunkler Modus aktivieren"
|
||||
|
||||
msgid "Extract..."
|
||||
msgstr "Entpacken..."
|
||||
@@ -1051,7 +1051,7 @@ msgid "Combine with the current data?"
|
||||
msgstr "Mit den aktuellen Daten kombinieren?"
|
||||
|
||||
msgid "Importing data done!"
|
||||
msgstr "Daten importieren erledigt!"
|
||||
msgstr "Daten-Import abgeschlossen!"
|
||||
|
||||
msgid "Current service"
|
||||
msgstr "Aktueller Service"
|
||||
@@ -1063,13 +1063,13 @@ msgid "Open archive"
|
||||
msgstr "Archiv öffnen"
|
||||
|
||||
msgid "Import from Web"
|
||||
msgstr "Import aus dem Web"
|
||||
msgstr "Import aus dem Internet"
|
||||
|
||||
msgid "Control"
|
||||
msgstr "Steuerung"
|
||||
|
||||
msgid "Timers"
|
||||
msgstr "Timers"
|
||||
msgstr "Timer"
|
||||
|
||||
msgid "Timer"
|
||||
msgstr "Timer"
|
||||
@@ -1111,7 +1111,7 @@ msgid "Auto"
|
||||
msgstr "Auto"
|
||||
|
||||
msgid "Grab screenshot"
|
||||
msgstr "Screenshot schnappen"
|
||||
msgstr "Screenshot aufnehmen"
|
||||
|
||||
msgid "Enabled:"
|
||||
msgstr "Aktiviert:"
|
||||
@@ -1138,7 +1138,7 @@ msgid "Ends:"
|
||||
msgstr "Endet:"
|
||||
|
||||
msgid "Repeated:"
|
||||
msgstr "Wiederhole:"
|
||||
msgstr "Wiederholt:"
|
||||
|
||||
msgid "Action:"
|
||||
msgstr "Aktion:"
|
||||
@@ -1172,3 +1172,72 @@ msgstr "So"
|
||||
|
||||
msgid "Set"
|
||||
msgstr "Einstellen"
|
||||
|
||||
msgid "Services update"
|
||||
msgstr "Dienste-Update"
|
||||
|
||||
msgid "Create folder"
|
||||
msgstr "Ordner erstellen"
|
||||
|
||||
msgid "FTP client"
|
||||
msgstr "FTP-Client"
|
||||
|
||||
msgid "The file size is too large!"
|
||||
msgstr "Die Datei ist zu groß!"
|
||||
|
||||
msgid "Connect"
|
||||
msgstr "Verbinden"
|
||||
|
||||
msgid "Disconnect"
|
||||
msgstr "Verbindung trennen"
|
||||
|
||||
msgid "Size"
|
||||
msgstr "Größe"
|
||||
|
||||
msgid "Date"
|
||||
msgstr "Datum"
|
||||
|
||||
msgid "Attr."
|
||||
msgstr "Attr."
|
||||
|
||||
msgid "Toggle display position"
|
||||
msgstr "Anzeigeposition umschalten"
|
||||
|
||||
msgid "Alternatives"
|
||||
msgstr "Alternativen"
|
||||
|
||||
msgid "Add alternatives"
|
||||
msgstr "Alternativen hinzufügen"
|
||||
|
||||
msgid "DreamOS only!"
|
||||
msgstr "Nur DreamOS!"
|
||||
|
||||
msgid "A similar service is already in this list!"
|
||||
msgstr "Ein ähnlicher Dienst ist bereits in dieser Liste enthalten!"
|
||||
|
||||
msgid "Play mode has been changed!\nRestart the program to apply the settings."
|
||||
msgstr "Abspiel-Modus wurde geändert!\nStarte das Programm neu, um die Einstellungen zu übernehmen."
|
||||
|
||||
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
|
||||
msgstr "Stelle die Werte für TID, NID und Namespace für die korrekte Benennung der Picons ein!"
|
||||
|
||||
msgid "Streams detected:"
|
||||
msgstr "Streams gefunden:"
|
||||
|
||||
msgid "Download picons"
|
||||
msgstr "Download picons"
|
||||
|
||||
msgid "Errors:"
|
||||
msgstr "Fehler:"
|
||||
|
||||
msgid "Use to play streams:"
|
||||
msgstr "Zum Abspielen von Streams verwenden:"
|
||||
|
||||
msgid "Font in the lists:"
|
||||
msgstr "Schrift in den Listen:"
|
||||
|
||||
msgid "Picons size in the lists:"
|
||||
msgstr "Picons Größe in den Listen:"
|
||||
|
||||
msgid "Logo size in tooltips:"
|
||||
msgstr "Logo-Größe in Tooltips:"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 Dmitriy Yefremov
|
||||
# Copyright (C) 2018-2021 Dmitriy Yefremov
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
#
|
||||
@@ -914,8 +914,8 @@ msgstr "Режим воспроизведения потоков:"
|
||||
msgid "Built-in player"
|
||||
msgstr "Встроенный плеер"
|
||||
|
||||
msgid "VLC media player"
|
||||
msgstr "VLC медиаплеер"
|
||||
msgid "In a separate window"
|
||||
msgstr "В отдельном окне"
|
||||
|
||||
msgid "Only get m3u file"
|
||||
msgstr "Получить файл *.m3u"
|
||||
@@ -1159,3 +1159,69 @@ msgstr "Вс"
|
||||
|
||||
msgid "Set"
|
||||
msgstr "Установить"
|
||||
|
||||
msgid "Services update"
|
||||
msgstr "Обновление сервисов"
|
||||
|
||||
msgid "Create folder"
|
||||
msgstr "Создать папку"
|
||||
|
||||
msgid "FTP client"
|
||||
msgstr "FTP-клиент"
|
||||
|
||||
msgid "The file size is too large!"
|
||||
msgstr "Размер файла слишком велик!"
|
||||
|
||||
msgid "Connect"
|
||||
msgstr "Соединение"
|
||||
|
||||
msgid "Disconnect"
|
||||
msgstr "Разъединить"
|
||||
|
||||
msgid "Size"
|
||||
msgstr "Размер"
|
||||
|
||||
msgid "Date"
|
||||
msgstr "Дата"
|
||||
|
||||
msgid "Toggle display position"
|
||||
msgstr "Переключить позицию отображения"
|
||||
|
||||
msgid "Alternatives"
|
||||
msgstr "Альтернативы"
|
||||
|
||||
msgid "Add alternatives"
|
||||
msgstr "Добавить альтернативы"
|
||||
|
||||
msgid "DreamOS only!"
|
||||
msgstr "Только DreamOS!"
|
||||
|
||||
msgid "A similar service is already in this list!"
|
||||
msgstr "Подобный сервис уже есть в этом списке!"
|
||||
|
||||
msgid "Play mode has been changed!\nRestart the program to apply the settings."
|
||||
msgstr "Изменен режим воспроизведения!\nПерезапустите программу для применения настроек."
|
||||
|
||||
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
|
||||
msgstr "Установите значения TID, NID и пр. имен для правильного именования пиконов!"
|
||||
|
||||
msgid "Streams detected:"
|
||||
msgstr "Обнаружено потоков:"
|
||||
|
||||
msgid "Download picons"
|
||||
msgstr "Загрузить пиконы"
|
||||
|
||||
msgid "Errors:"
|
||||
msgstr "Ошибок:"
|
||||
|
||||
msgid "Use to play streams:"
|
||||
msgstr "Использовать для воспроизведения потоков:"
|
||||
|
||||
msgid "Font in the lists:"
|
||||
msgstr "Шрифт в списках:"
|
||||
|
||||
msgid "Picons size in the lists:"
|
||||
msgstr "Размер пиконов в списках:"
|
||||
|
||||
msgid "Logo size in tooltips:"
|
||||
msgstr "Размер логотипа во всплывающих подсказках:"
|
||||
|
||||
@@ -3,13 +3,13 @@ msgstr ""
|
||||
"Project-Id-Version: DemonEditor\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-04-16 15:59+0300\n"
|
||||
"PO-Revision-Date: 2020-06-08 21:53+0300\n"
|
||||
"PO-Revision-Date: 2021-02-22 23:53+0300\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: audi06_19 <info@dreamosat-forum.com>\n"
|
||||
"Language-Team: \n"
|
||||
"X-Generator: Poedit 2.2.1\n"
|
||||
"X-Generator: Poedit 2.4.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Language: tr\n"
|
||||
|
||||
@@ -107,6 +107,9 @@ msgstr "Varsayılan adı ayarla"
|
||||
msgid "Insert marker"
|
||||
msgstr "İşaretçi ekle"
|
||||
|
||||
msgid "Insert space"
|
||||
msgstr "Boşluk ekle"
|
||||
|
||||
msgid "Locate in services"
|
||||
msgstr "Hizmetlerde bulun"
|
||||
|
||||
@@ -518,6 +521,9 @@ msgstr "Lütfen sadece bir ürün seçiniz!"
|
||||
msgid "No png file is selected!"
|
||||
msgstr "Hiçbir png dosyası seçilmedi!"
|
||||
|
||||
msgid "No profile selected!"
|
||||
msgstr "Profil seçilmedi!"
|
||||
|
||||
msgid "No reference is present!"
|
||||
msgstr "Referans yok!"
|
||||
|
||||
@@ -923,8 +929,8 @@ msgstr "Akışları oynatma modu:"
|
||||
msgid "Built-in player"
|
||||
msgstr "Dahili oynatıcı"
|
||||
|
||||
msgid "VLC media player"
|
||||
msgstr "VLC media player"
|
||||
msgid "In a separate window"
|
||||
msgstr "Ayrı bir pencerede"
|
||||
|
||||
msgid "Only get m3u file"
|
||||
msgstr "Sadece m3u dosyası al"
|
||||
@@ -988,3 +994,254 @@ msgstr "Alıcıdaki tüm piconları kaldırın"
|
||||
|
||||
msgid "Service reference"
|
||||
msgstr "Servis referansı"
|
||||
|
||||
msgid "Enable support for"
|
||||
msgstr "Temalar için desteği etkinleştir"
|
||||
|
||||
msgid "Auto-check for updates"
|
||||
msgstr "Güncellemeleri otomatik kontrol et"
|
||||
|
||||
msgid "Filter services"
|
||||
msgstr "Services filtrele"
|
||||
|
||||
msgid "Filter services in the main list."
|
||||
msgstr "Ana listedeki services filtreleyin."
|
||||
|
||||
msgid "Destination:"
|
||||
msgstr "Hedef:"
|
||||
|
||||
msgid "EXPERIMENTAL!"
|
||||
msgstr "EXPERIMENTAL!"
|
||||
|
||||
msgid "Sorting data..."
|
||||
msgstr "Verilerin sıralanması..."
|
||||
|
||||
msgid ""
|
||||
"There are unsaved changes.\n"
|
||||
"\n"
|
||||
"\t Save them now?"
|
||||
msgstr ""
|
||||
"Kaydedilmemiş değişiklikler var.\n"
|
||||
"\n"
|
||||
"\tŞimdi kaydedilsin mi?"
|
||||
|
||||
msgid ""
|
||||
"Are you sure you want to change the order\n"
|
||||
"\t of services in this bouquet?"
|
||||
msgstr ""
|
||||
"Sırayı değiştirmek istediğinizden emin misiniz\n"
|
||||
"\t Bu buketdeki hizmetlerin sayısı?"
|
||||
|
||||
msgid "Remove from the receiver"
|
||||
msgstr "Alıcıdaki tüm piconları kaldırın"
|
||||
|
||||
msgid "Screenshot"
|
||||
msgstr "Ekran görüntüsü"
|
||||
|
||||
msgid "Video"
|
||||
msgstr "Video"
|
||||
|
||||
msgid "The Neutrino has only experimental support. Not all features are supported!"
|
||||
msgstr "Neutrino'nun yalnızca experimental desteği var. Tüm özellikler desteklenmiyor!"
|
||||
|
||||
msgid "Enable experimental features"
|
||||
msgstr "Experimental özellikleri etkinleştirin"
|
||||
|
||||
msgid "Can't Playback!"
|
||||
msgstr "Oynatılamıyor!"
|
||||
|
||||
msgid "Enable Dark Mode"
|
||||
msgstr "Koyu Modu Etkinleştir"
|
||||
|
||||
msgid "Extract..."
|
||||
msgstr "Çıkart..."
|
||||
|
||||
msgid "Unsupported format!"
|
||||
msgstr "Desteklenmeyen format!"
|
||||
|
||||
msgid "Combine with the current data?"
|
||||
msgstr "Mevcut verilerle birleştirilsin mi?"
|
||||
|
||||
msgid "Importing data done!"
|
||||
msgstr "Verilerin içe aktarılması tamamlandı!"
|
||||
|
||||
msgid "Current service"
|
||||
msgstr "Mevcut service"
|
||||
|
||||
msgid "Open folder"
|
||||
msgstr "Dosya aç"
|
||||
|
||||
msgid "Open archive"
|
||||
msgstr "Arşiv aç"
|
||||
|
||||
msgid "Import from Web"
|
||||
msgstr "Web'den içe aktar"
|
||||
|
||||
msgid "Control"
|
||||
msgstr "Control"
|
||||
|
||||
msgid "Timers"
|
||||
msgstr "Zamanlayıcılar"
|
||||
|
||||
msgid "Timer"
|
||||
msgstr "Zamanlayıcı"
|
||||
|
||||
msgid "Add timer"
|
||||
msgstr "Zamanlayıcı ekle"
|
||||
|
||||
msgid "Hr."
|
||||
msgstr "Hr."
|
||||
|
||||
msgid "Min."
|
||||
msgstr "Min."
|
||||
|
||||
msgid "Power"
|
||||
msgstr "Power"
|
||||
|
||||
msgid "Standby"
|
||||
msgstr "Standby"
|
||||
|
||||
msgid "Wake Up"
|
||||
msgstr "Wake Up"
|
||||
|
||||
msgid "Reboot"
|
||||
msgstr "Reboot"
|
||||
|
||||
msgid "Restart GUI"
|
||||
msgstr "Restart GUI"
|
||||
|
||||
msgid "Shutdown"
|
||||
msgstr "Shutdown"
|
||||
|
||||
msgid "Shut down"
|
||||
msgstr "Kapat"
|
||||
|
||||
msgid "Do Nothing"
|
||||
msgstr "Hiçbir şey yapma"
|
||||
|
||||
msgid "Auto"
|
||||
msgstr "Auto"
|
||||
|
||||
msgid "Grab screenshot"
|
||||
msgstr "Ekran görüntüsü al"
|
||||
|
||||
msgid "Enabled:"
|
||||
msgstr "Etkin:"
|
||||
|
||||
msgid "Name:"
|
||||
msgstr "Ad:"
|
||||
|
||||
msgid "Description:"
|
||||
msgstr "Açıklama:"
|
||||
|
||||
msgid "Service:"
|
||||
msgstr "Service:"
|
||||
|
||||
msgid "Service reference:"
|
||||
msgstr "Servis referansı:"
|
||||
|
||||
msgid "Event ID:"
|
||||
msgstr "Olay Kimliği:"
|
||||
|
||||
msgid "Begins:"
|
||||
msgstr "Başlıyor:"
|
||||
|
||||
msgid "Ends:"
|
||||
msgstr "Bitiş:"
|
||||
|
||||
msgid "Repeated:"
|
||||
msgstr "Tekrar:"
|
||||
|
||||
msgid "Action:"
|
||||
msgstr "Aksiyon:"
|
||||
|
||||
msgid "After event:"
|
||||
msgstr "Olaydan sonra:"
|
||||
|
||||
msgid "Location:"
|
||||
msgstr "Konum:"
|
||||
|
||||
msgid "Mo"
|
||||
msgstr "Pzt"
|
||||
|
||||
msgid "Tu"
|
||||
msgstr "Sal"
|
||||
|
||||
msgid "We"
|
||||
msgstr "Çar"
|
||||
|
||||
msgid "Th"
|
||||
msgstr "Per"
|
||||
|
||||
msgid "Fr"
|
||||
msgstr "Cum"
|
||||
|
||||
msgid "Sa"
|
||||
msgstr "Cmt"
|
||||
|
||||
msgid "Su"
|
||||
msgstr "Paz"
|
||||
|
||||
msgid "Set"
|
||||
msgstr "Yüklemek"
|
||||
|
||||
msgid "Services update"
|
||||
msgstr "Servisleri güncelle"
|
||||
|
||||
msgid "Create folder"
|
||||
msgstr "Klasör oluştur"
|
||||
|
||||
msgid "FTP client"
|
||||
msgstr "FTP istemcisi"
|
||||
|
||||
msgid "The file size is too large!"
|
||||
msgstr "Dosya boyutu çok büyük!"
|
||||
|
||||
msgid "Connect"
|
||||
msgstr "Bağlan"
|
||||
|
||||
msgid "Disconnect"
|
||||
msgstr "Bağlantıyı kes"
|
||||
|
||||
msgid "Size"
|
||||
msgstr "Boyut"
|
||||
|
||||
msgid "Date"
|
||||
msgstr "Saat"
|
||||
|
||||
msgid "Attr."
|
||||
msgstr "Özellik."
|
||||
|
||||
msgid "Toggle display position"
|
||||
msgstr "Görüntü konumunu değiştir"
|
||||
|
||||
msgid "Alternatives"
|
||||
msgstr "Alternatifler"
|
||||
|
||||
msgid "Add alternatives"
|
||||
msgstr "Alternatif ekleyin"
|
||||
|
||||
msgid "DreamOS only!"
|
||||
msgstr "Sadece DreamOS!"
|
||||
|
||||
msgid "A similar service is already in this list!"
|
||||
msgstr "Bu listede zaten benzer bir hizmet var!"
|
||||
|
||||
msgid ""
|
||||
"Play mode has been changed!\n"
|
||||
"Restart the program to apply the settings."
|
||||
msgstr ""
|
||||
"Oynatma modu değiştirildi!\n"
|
||||
"Ayarları uygulamak için programı yeniden başlatın."
|
||||
|
||||
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
|
||||
msgstr "Piconların doğru isimlendirilmesi için TID, NID ve Namespace değerlerini ayarlayın!"
|
||||
|
||||
msgid "Streams detected:"
|
||||
msgstr "Akışlar algılandı:"
|
||||
|
||||
msgid "Download picons"
|
||||
msgstr "Picon'lar indirin"
|
||||
|
||||
msgid "Errors:"
|
||||
msgstr "Hatalar:"
|
||||
|
||||
16
start.py
16
start.py
@@ -1,19 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
try:
|
||||
from Cocoa import NSBundle
|
||||
except ImportError as e:
|
||||
print(e)
|
||||
else:
|
||||
ns_bundle = NSBundle.mainBundle()
|
||||
if ns_bundle:
|
||||
ns_bundle = ns_bundle.localizedInfoDictionary() or ns_bundle.infoDictionary()
|
||||
if ns_bundle:
|
||||
ns_bundle["CFBundleName"] = "DemonEditor"
|
||||
|
||||
if __name__ == "__main__":
|
||||
from multiprocessing import set_start_method
|
||||
from multiprocessing import freeze_support
|
||||
from app.ui.main_app_window import start_app
|
||||
|
||||
set_start_method("fork") # For compatibility [Python > 3.7]
|
||||
freeze_support()
|
||||
start_app()
|
||||
|
||||
Reference in New Issue
Block a user