added basic support for downloading picons from picon.cz

This commit is contained in:
DYefremov
2021-05-11 00:18:27 +03:00
parent 73a1f2ccef
commit a99c1e00b9
4 changed files with 529 additions and 133 deletions

View File

@@ -1,7 +1,36 @@
# -*- coding: utf-8 -*-
#
# 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
#
import glob import glob
import os import os
import re import re
import shutil import shutil
import subprocess
from collections import namedtuple from collections import namedtuple
from html.parser import HTMLParser from html.parser import HTMLParser
@@ -18,6 +47,166 @@ Provider = namedtuple("Provider", ["logo", "name", "pos", "url", "on_id", "ssid"
Picon = namedtuple("Picon", ["ref", "ssid"]) Picon = namedtuple("Picon", ["ref", "ssid"])
class PiconsError(Exception):
pass
class PiconsCzDownloader:
""" The main class for loading picons from the https://picon.cz/ source (by Chocholoušek). """
_PERM_URL = "https://picon.cz/download/7337"
_BASE_URL = "https://picon.cz/download/"
_BASE_LOGO_URL = "https://picon.cz/picon/0/"
_HEADER = {"User-Agent": "DemonEditor/1.0.8", "Referer": ""}
_LINK_PATTERN = re.compile(r"((.*)-\d+x\d+)-(.*)_by_chocholousek.7z$")
_FILE_PATTERN = re.compile(b"\\s+(1_.*\\.png).*")
def __init__(self, picon_ids=set(), appender=log):
self._perm_links = {}
self._providers = {}
self._provider_logos = {}
self._picon_ids = picon_ids
self._appender = appender
def init(self):
""" Initializes dict with values: download_id -> perm link and provider data. """
if self._perm_links:
return
self._HEADER["Referer"] = self._PERM_URL
with requests.get(url=self._PERM_URL, headers=self._HEADER, stream=True) as request:
if request.reason == "OK":
logo_map = self.get_logos_map()
name_map = self.get_name_map()
for line in request.iter_lines():
l_id, perm_link = line.decode(encoding="utf-8", errors="ignore").split(maxsplit=1)
self._perm_links[str(l_id)] = str(perm_link)
data = re.match(self._LINK_PATTERN, perm_link)
if data:
sat_pos = data.group(3)
# Logo url.
logo = logo_map.get(data.group(2), None)
l_name = name_map.get(sat_pos, None) or sat_pos.replace(".", "")
logo_url = "{}{}/{}.png".format(self._BASE_LOGO_URL, logo, l_name) if logo else None
prv = Provider(None, data.group(1), sat_pos, self._BASE_URL + l_id, l_id, logo_url, None, False)
if sat_pos in self._providers:
self._providers[sat_pos].append(prv)
else:
self._providers[sat_pos] = [prv]
else:
log("{} [get permalinks] error: {}".format(self.__class__.__name__, request.reason))
raise PiconsError(request.reason)
@property
def providers(self):
return self._providers
def get_sat_providers(self, url):
return self._providers.get(url, [])
def download(self, provider, picons_path):
self._HEADER["Referer"] = provider.url
with requests.get(url=provider.url, headers=self._HEADER, stream=True) as request:
if request.reason == "OK":
dest = "{}{}.7z".format(picons_path, provider.on_id)
self._appender("Downloading: {}\n".format(provider.url))
with open(dest, mode="bw") as f:
for data in request.iter_content(chunk_size=1024):
f.write(data)
self._appender("Extracting: {}\n".format(provider.on_id))
self.extract(dest, picons_path)
else:
log("{} [download] error: {}".format(self.__class__.__name__, request.reason))
def extract(self, src, dest):
""" Extracts 7z archives. """
# TODO: think about https://github.com/miurahr/py7zr
cmd = ["7zr", "l", src]
try:
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
if err:
log("{} [extract] error: {}".format(self.__class__.__name__, err))
raise PiconsError(err)
except OSError as e:
log("{} [extract] error: {}".format(self.__class__.__name__, e))
raise PiconsError(e)
to_extract = []
for o in re.finditer(self._FILE_PATTERN, out):
p_id = o.group(1).decode("utf-8", errors="ignore")
if p_id in self._picon_ids:
to_extract.append(p_id)
cmd = ["7zr", "e", src, "-o{}".format(dest), "-y", "-r"]
cmd.extend(to_extract)
try:
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
if err:
log("{} [extract] error: {}".format(self.__class__.__name__, err))
raise PiconsError(err)
else:
if os.path.isfile(src):
os.remove(src)
except OSError as e:
log(e)
raise PiconsError(e)
def get_logo_data(self, url):
""" Returns the logo data if present. """
return self._provider_logos.get(url, None)
def get_provider_logo(self, url):
""" Retrieves package logo. """
# Getting package logo.
logo = self._provider_logos.get(url, None)
if logo:
return logo
with requests.get(url=url, stream=True) as logo_request:
if logo_request.reason == "OK":
data = logo_request.content
self._provider_logos[url] = data
return data
else:
log("Downloading package logo error: {}".format(logo_request.reason))
def get_logos_map(self):
return {"piconblack": "b50",
"picontransparent": "t50",
"piconwhite": "w50",
"piconmirrorglass": "mr100",
"piconnoName": "n100",
"piconsrhd": "srhd100",
"piconfreezeframe": "ff220",
"piconfreezewhite": "fw100",
"piconpoolrainbow": "r100",
"piconsimpleblack": "s220",
"piconjustblack": "jb220",
"picondirtypaper": "dp220",
"picongray": "g400",
"piconmonochrom": "m220",
"picontransparentwhite": "tw100",
"picontransparentdark": "td220",
"piconoled": "o96",
"piconblack80": "b50",
"piconblack3d": "b50"
}
def get_name_map(self):
return {"antiksat": "ANTIK",
"digiczsk": "DIGI",
"DTTitaly": "picon_trs-it",
"dvbtCZSK": "picon_trs",
"PolandDTT": "picon_trs-pl",
"freeSAT": "UPC DIRECT",
"orangesat": "ORANGE TV",
"skylink": "M7 GROUP",
}
class PiconsParser(HTMLParser): class PiconsParser(HTMLParser):
""" Parser for package html page. (https://www.lyngsat.com/packages/*provider-name*.html) """ """ Parser for package html page. (https://www.lyngsat.com/packages/*provider-name*.html) """
_BASE_URL = "https://www.lyngsat.com" _BASE_URL = "https://www.lyngsat.com"

View File

@@ -1,3 +1,31 @@
# -*- coding: utf-8 -*-
#
# 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
#
import os import os
import sys import sys
from contextlib import suppress from contextlib import suppress
@@ -1853,7 +1881,8 @@ class Application(Gtk.Application):
alt_servs = srv.transponder alt_servs = srv.transponder
if alt_servs: if alt_servs:
alt_srv = self._services.get(alt_servs[0].data, None) alt_srv = self._services.get(alt_servs[0].data, None)
picon = self._picons.get(alt_srv.picon_id, None) if srv else None if alt_srv:
picon = self._picons.get(alt_srv.picon_id, None) if srv else None
self._fav_model.append((0 if is_marker else num, srv.coded, ex_srv_name if ex_srv_name else srv.service, self._fav_model.append((0 if is_marker else num, srv.coded, ex_srv_name if ex_srv_name else srv.service,
srv.locked, srv.hide, srv_type, srv.pos, srv.fav_id, srv.locked, srv.hide, srv_type, srv.pos, srv.fav_id,
@@ -2294,6 +2323,7 @@ class Application(Gtk.Application):
dialog = ImportDialog(self._main_window, path, self._settings, self._services.keys(), append) dialog = ImportDialog(self._main_window, path, self._settings, self._services.keys(), append)
dialog.import_data() if force else dialog.show() dialog.import_data() if force else dialog.show()
self.update_picons()
def append_imported_data(self, bouquets, services, callback=None): def append_imported_data(self, bouquets, services, callback=None):
try: try:

View File

@@ -3,7 +3,7 @@
The MIT License (MIT) 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -28,9 +28,10 @@ Author: Dmitriy Yefremov
--> -->
<interface domain="demon-editor"> <interface domain="demon-editor">
<requires lib="gtk+" version="3.16"/> <requires lib="gtk+" version="3.16"/>
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit --> <!-- interface-license-type mit -->
<!-- interface-name DemonEditor --> <!-- interface-name DemonEditor -->
<!-- interface-copyright 2018-2020 Dmitriy Yefremov --> <!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov --> <!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="picons_dest_list_store"> <object class="GtkListStore" id="picons_dest_list_store">
<columns> <columns>
@@ -228,8 +229,20 @@ Author: Dmitriy Yefremov
<object class="GtkHeaderBar" id="header"> <object class="GtkHeaderBar" id="header">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property> <property name="show_close_button">True</property>
<child>
<object class="GtkComboBoxText" id="download_source_button">
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Source:</property>
<property name="active">0</property>
<items>
<item id="piconcz" translatable="yes">Picon.cz</item>
<item id="lyngsat" translatable="yes">LyngSat</item>
</items>
<signal name="changed" handler="on_download_source_changed" swapped="no"/>
</object>
</child>
<child> <child>
<object class="GtkButton" id="cancel_button"> <object class="GtkButton" id="cancel_button">
<property name="can_focus">False</property> <property name="can_focus">False</property>
@@ -246,35 +259,15 @@ Author: Dmitriy Yefremov
</child> </child>
<accelerator key="z" signal="clicked" modifiers="GDK_CONTROL_MASK"/> <accelerator key="z" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object> </object>
<packing>
<property name="position">1</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkBox" id="header_download_box"> <object class="GtkBox" id="header_download_box">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="spacing">2</property> <property name="spacing">2</property>
<child>
<object class="GtkButton" id="load_providers_button">
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Load providers</property>
<property name="halign">center</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_load_providers" swapped="no"/>
<child>
<object class="GtkImage" id="load_providers_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">network-server-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child> <child>
<object class="GtkButton" id="receive_button"> <object class="GtkButton" id="receive_button">
<property name="sensitive">False</property> <property name="sensitive">False</property>
@@ -398,27 +391,6 @@ Author: Dmitriy Yefremov
<property name="position">4</property> <property name="position">4</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkCheckButton" id="info_check_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Details</property>
<property name="draw_indicator">False</property>
<child>
<object class="GtkImage" id="info_check_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-dialog-info</property>
</object>
</child>
<accelerator key="i" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">5</property>
</packing>
</child>
<child> <child>
<object class="GtkToggleButton" id="filter_button"> <object class="GtkToggleButton" id="filter_button">
<property name="visible">True</property> <property name="visible">True</property>
@@ -441,6 +413,27 @@ Author: Dmitriy Yefremov
<property name="position">4</property> <property name="position">4</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkCheckButton" id="info_check_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Details</property>
<property name="draw_indicator">False</property>
<child>
<object class="GtkImage" id="info_check_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-dialog-info</property>
</object>
</child>
<accelerator key="i" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">5</property>
</packing>
</child>
</object> </object>
</child> </child>
<child> <child>
@@ -995,7 +988,7 @@ Author: Dmitriy Yefremov
<property name="wide_handle">True</property> <property name="wide_handle">True</property>
<child> <child>
<object class="GtkBox" id="satellites_box"> <object class="GtkBox" id="satellites_box">
<property name="width_request">200</property> <property name="width_request">250</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_right">2</property> <property name="margin_right">2</property>
@@ -1042,6 +1035,48 @@ Author: Dmitriy Yefremov
<property name="position">2</property> <property name="position">2</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkGrid" id="satellite_filter_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">5</property>
<property name="column_spacing">2</property>
<child>
<object class="GtkCheckButton" id="satellite_filter_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="valign">center</property>
<property name="image_position">right</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_satellite_filter_toggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="satellite_filter_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Filter</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">3</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -1127,10 +1162,11 @@ Author: Dmitriy Yefremov
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<property name="spacing">2</property> <property name="spacing">2</property>
<child> <child>
<object class="GtkLabel"> <object class="GtkLabel" id="provider_header_label">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="label" translatable="yes">Satellite url:</property> <property name="halign">start</property>
<property name="margin_left">10</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -1138,22 +1174,6 @@ Author: Dmitriy Yefremov
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkEntry" id="url_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">network-workgroup-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="placeholder_text" translatable="yes">https://www.lyngsat.com/*satellite*.html</property>
<property name="input_purpose">url</property>
<signal name="changed" handler="on_url_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child> <child>
<object class="GtkScrolledWindow" id="providers_scrolled_window"> <object class="GtkScrolledWindow" id="providers_scrolled_window">
<property name="height_request">150</property> <property name="height_request">150</property>
@@ -1167,7 +1187,9 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="model">providers_list_store</property> <property name="model">providers_list_store</property>
<property name="search_column">1</property> <property name="search_column">1</property>
<property name="tooltip_column">0</property>
<signal name="button-press-event" handler="on_popup_menu" object="providers_popup_menu" swapped="no"/> <signal name="button-press-event" handler="on_popup_menu" object="providers_popup_menu" swapped="no"/>
<signal name="query-tooltip" handler="on_providers_view_query_tooltip" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/> <signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection"> <child internal-child="selection">
<object class="GtkTreeSelection"/> <object class="GtkTreeSelection"/>
@@ -1175,11 +1197,14 @@ Author: Dmitriy Yefremov
<child> <child>
<object class="GtkTreeViewColumn" id="provider_column"> <object class="GtkTreeViewColumn" id="provider_column">
<property name="spacing">15</property> <property name="spacing">15</property>
<property name="title" translatable="yes">Providers</property> <property name="title" translatable="yes">Name</property>
<property name="expand">True</property> <property name="expand">True</property>
<property name="alignment">0.5</property> <property name="alignment">0.5</property>
<property name="sort_column_id">1</property>
<child> <child>
<object class="GtkCellRendererPixbuf" id="logo_cellrendererpixbuf"/> <object class="GtkCellRendererPixbuf" id="logo_cellrendererpixbuf">
<property name="xpad">5</property>
</object>
<attributes> <attributes>
<attribute name="pixbuf">0</attribute> <attribute name="pixbuf">0</attribute>
</attributes> </attributes>
@@ -1261,6 +1286,7 @@ Author: Dmitriy Yefremov
<child> <child>
<object class="GtkTreeViewColumn" id="selected_column"> <object class="GtkTreeViewColumn" id="selected_column">
<property name="title" translatable="yes">Selected</property> <property name="title" translatable="yes">Selected</property>
<property name="sort_column_id">7</property>
<child> <child>
<object class="GtkCellRendererToggle" id="cellrenderer_toggle"> <object class="GtkCellRendererToggle" id="cellrenderer_toggle">
<signal name="toggled" handler="on_selected_toggled" swapped="no"/> <signal name="toggled" handler="on_selected_toggled" swapped="no"/>

View File

@@ -1,7 +1,37 @@
# -*- coding: utf-8 -*-
#
# 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
#
import concurrent.futures
import os import os
import re import re
import shutil import shutil
import tempfile import tempfile
from enum import Enum
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse, unquote from urllib.parse import urlparse, unquote
@@ -10,7 +40,8 @@ from gi.repository import GLib, GdkPixbuf, Gio
from app.commons import run_idle, run_task, run_with_delay from app.commons import run_idle, run_task, run_with_delay
from app.connections import upload_data, DownloadType, download_data, remove_picons from app.connections import upload_data, DownloadType, download_data, remove_picons
from app.settings import SettingsType, Settings from app.settings import SettingsType, Settings
from app.tools.picons import PiconsParser, parse_providers, Provider, convert_to, download_picon from app.tools.picons import (PiconsParser, parse_providers, Provider, convert_to, download_picon, PiconsCzDownloader,
PiconsError)
from app.tools.satellites import SatellitesParser, SatelliteSource from app.tools.satellites import SatellitesParser, SatelliteSource
from .dialogs import show_dialog, DialogType, get_message, get_builder from .dialogs import show_dialog, DialogType, get_message, get_builder
from .main_helper import (update_entry_data, append_text_to_tview, scroll_to, on_popup_menu, get_base_model, set_picon, from .main_helper import (update_entry_data, append_text_to_tview, scroll_to, on_popup_menu, get_base_model, set_picon,
@@ -19,6 +50,10 @@ from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey
class PiconsDialog: class PiconsDialog:
class DownloadSource(Enum):
LYNG_SAT = "lyngsat"
PICON_CZ = "piconcz"
def __init__(self, transient, settings, picon_ids, sat_positions, app): def __init__(self, transient, settings, picon_ids, sat_positions, app):
self._picon_ids = picon_ids self._picon_ids = picon_ids
self._sat_positions = sat_positions self._sat_positions = sat_positions
@@ -33,9 +68,13 @@ class PiconsDialog:
self._filter_binding = None self._filter_binding = None
self._services = None self._services = None
self._current_picon_info = None self._current_picon_info = None
# Downloader
self._sats = None
self._sat_names = None
self._download_src = self.DownloadSource.PICON_CZ
self._picon_cz_downloader = None
handlers = {"on_receive": self.on_receive, handlers = {"on_receive": self.on_receive,
"on_load_providers": self.on_load_providers,
"on_cancel": self.on_cancel, "on_cancel": self.on_cancel,
"on_close": self.on_close, "on_close": self.on_close,
"on_send": self.on_send, "on_send": self.on_send,
@@ -64,7 +103,10 @@ class PiconsDialog:
"on_selective_remove": self.on_selective_remove, "on_selective_remove": self.on_selective_remove,
"on_local_remove": self.on_local_remove, "on_local_remove": self.on_local_remove,
"on_picons_dest_view_realize": self.on_picons_dest_view_realize, "on_picons_dest_view_realize": self.on_picons_dest_view_realize,
"on_download_source_changed": self.on_download_source_changed,
"on_satellites_view_realize": self.on_satellites_view_realize, "on_satellites_view_realize": self.on_satellites_view_realize,
"on_satellite_filter_toggled": self.on_satellite_filter_toggled,
"on_providers_view_query_tooltip": self.on_providers_view_query_tooltip,
"on_satellite_selection": self.on_satellite_selection, "on_satellite_selection": self.on_satellite_selection,
"on_select_all": self.on_select_all, "on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all, "on_unselect_all": self.on_unselect_all,
@@ -100,7 +142,6 @@ class PiconsDialog:
self._picons_filter_entry = builder.get_object("picons_filter_entry") self._picons_filter_entry = builder.get_object("picons_filter_entry")
self._ip_entry = builder.get_object("ip_entry") self._ip_entry = builder.get_object("ip_entry")
self._picons_entry = builder.get_object("picons_entry") self._picons_entry = builder.get_object("picons_entry")
self._url_entry = builder.get_object("url_entry")
self._picons_dir_entry = builder.get_object("picons_dir_entry") self._picons_dir_entry = builder.get_object("picons_dir_entry")
self._info_bar = builder.get_object("info_bar") self._info_bar = builder.get_object("info_bar")
self._info_bar = builder.get_object("info_bar") self._info_bar = builder.get_object("info_bar")
@@ -108,7 +149,7 @@ class PiconsDialog:
self._info_check_button = builder.get_object("info_check_button") self._info_check_button = builder.get_object("info_check_button")
self._picon_info_image = builder.get_object("picon_info_image") self._picon_info_image = builder.get_object("picon_info_image")
self._picon_info_label = builder.get_object("picon_info_label") self._picon_info_label = builder.get_object("picon_info_label")
self._load_providers_button = builder.get_object("load_providers_button") self._download_source_button = builder.get_object("download_source_button")
self._receive_button = builder.get_object("receive_button") self._receive_button = builder.get_object("receive_button")
self._convert_button = builder.get_object("convert_button") self._convert_button = builder.get_object("convert_button")
self._enigma2_path_button = builder.get_object("enigma2_path_button") self._enigma2_path_button = builder.get_object("enigma2_path_button")
@@ -123,12 +164,16 @@ class PiconsDialog:
self._resize_220_132_radio_button = builder.get_object("resize_220_132_radio_button") self._resize_220_132_radio_button = builder.get_object("resize_220_132_radio_button")
self._resize_100_60_radio_button = builder.get_object("resize_100_60_radio_button") self._resize_100_60_radio_button = builder.get_object("resize_100_60_radio_button")
self._satellite_label = builder.get_object("satellite_label") self._satellite_label = builder.get_object("satellite_label")
self._provider_header_label = builder.get_object("provider_header_label")
self._satellite_filter_button = builder.get_object("satellite_filter_button")
self._header_download_box = builder.get_object("header_download_box") self._header_download_box = builder.get_object("header_download_box")
self._satellite_label.bind_property("visible", builder.get_object("loading_data_label"), "visible", 4) self._satellite_label.bind_property("visible", builder.get_object("loading_data_label"), "visible", 4)
self._satellite_label.bind_property("visible", builder.get_object("loading_data_spinner"), "visible", 4) self._satellite_label.bind_property("visible", builder.get_object("loading_data_spinner"), "visible", 4)
self._satellite_label.bind_property("visible", self._download_source_button, "sensitive")
self._satellite_label.bind_property("visible", self._satellites_view, "sensitive")
self._cancel_button.bind_property("visible", self._header_download_box, "visible", 4) self._cancel_button.bind_property("visible", self._header_download_box, "visible", 4)
self._convert_button.bind_property("visible", self._header_download_box, "visible", 4) self._convert_button.bind_property("visible", self._header_download_box, "visible", 4)
self._load_providers_button.bind_property("visible", self._receive_button, "visible") self._download_source_button.bind_property("visible", self._receive_button, "visible")
self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible") self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible")
self._explorer_src_path_button.bind_property("sensitive", builder.get_object("picons_view_sw"), "sensitive") self._explorer_src_path_button.bind_property("sensitive", builder.get_object("picons_view_sw"), "sensitive")
self._filter_button.bind_property("active", builder.get_object("filter_service_box"), "visible") self._filter_button.bind_property("active", builder.get_object("filter_service_box"), "visible")
@@ -143,11 +188,7 @@ class PiconsDialog:
self._info_check_button.bind_property("active", explorer_info_bar, "visible") self._info_check_button.bind_property("active", explorer_info_bar, "visible")
# Init drag-and-drop # Init drag-and-drop
self.init_drag_and_drop() self.init_drag_and_drop()
# Style # Settings
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self._settings = settings self._settings = settings
self._s_type = settings.setting_type self._s_type = settings.setting_type
self._ip_entry.set_text(self._settings.host) self._ip_entry.set_text(self._settings.host)
@@ -464,72 +505,158 @@ class PiconsDialog:
# ******************** Downloader ************************* # # ******************** Downloader ************************* #
def on_download_source_changed(self, button):
self._download_src = self.DownloadSource(button.get_active_id())
self.set_providers_header()
GLib.idle_add(self._providers_view.get_model().clear)
self.init_satellites(self._satellites_view)
def on_satellites_view_realize(self, view): def on_satellites_view_realize(self, view):
self.set_providers_header()
self.get_satellites(view) self.get_satellites(view)
def on_satellite_filter_toggled(self, button):
self.init_satellites(self._satellites_view)
def on_providers_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
if self._download_src is self.DownloadSource.LYNG_SAT:
return False
dest = view.get_dest_row_at_pos(x, y)
if not dest:
return False
path, pos = dest
model = view.get_model()
itr = model.get_iter(path)
logo_url = model.get_value(itr, 5)
if logo_url:
pix_data = self._picon_cz_downloader.get_logo_data(logo_url)
if pix_data:
pix = self.get_pixbuf(pix_data)
model.set_value(itr, 0, pix if pix else TV_ICON)
size = self._settings.tooltip_logo_size
tooltip.set_icon(self.get_pixbuf(pix_data, size, size))
else:
self.update_logo_data(itr, model, logo_url)
tooltip.set_text(model.get_value(itr, 1))
view.set_tooltip_row(tooltip, path)
return True
@run_task
def update_logo_data(self, itr, model, url):
pix_data = self._picon_cz_downloader.get_provider_logo(url)
if pix_data:
pix = self.get_pixbuf(pix_data)
GLib.idle_add(model.set_value, itr, 0, pix if pix else TV_ICON)
@run_idle
def set_providers_header(self):
msg = "{} [{}]"
if self._download_src is self.DownloadSource.PICON_CZ:
msg = msg.format(get_message("Package"), "https://picon.cz (by Chocholoušek)")
elif self._download_src is self.DownloadSource.LYNG_SAT:
msg = msg.format(get_message("Providers"), "https://www.lyngsat.com")
else:
msg = ""
self._provider_header_label.set_text(msg)
@run_task @run_task
def get_satellites(self, view): def get_satellites(self, view):
sats = SatellitesParser().get_satellites_list(SatelliteSource.LYNGSAT) self._sats = SatellitesParser().get_satellites_list(SatelliteSource.LYNGSAT)
if not sats: if not self._sats:
self.show_info_message("Getting satellites list error!", Gtk.MessageType.ERROR) self.show_info_message("Getting satellites list error!", Gtk.MessageType.ERROR)
self._sat_names = {s[1]: s[0] for s in self._sats} # position -> satellite name
self._picon_cz_downloader = PiconsCzDownloader(self._picon_ids, self.append_output)
self.init_satellites(view)
@run_task
def init_satellites(self, view):
sats = self._sats
if self._download_src is self.DownloadSource.PICON_CZ:
try:
self._picon_cz_downloader.init()
except PiconsError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
providers = self._picon_cz_downloader.providers
sats = ((self._sat_names.get(p, p), p, None, p, False) for p in providers)
gen = self.append_satellites(view.get_model(), sats) gen = self.append_satellites(view.get_model(), sats)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def append_satellites(self, model, sats): def append_satellites(self, model, sats):
is_filter = self._satellite_filter_button.get_active()
model.clear()
try: try:
for sat in sats: for sat in sorted(sats):
pos = sat[1] pos = sat[1]
name = "{} ({})".format(sat[0], pos) name = "{} ({})".format(sat[0], pos)
if not self._terminate and model: if not self._terminate and model:
if pos in self._sat_positions: if is_filter and pos not in self._sat_positions:
yield model.append((name, sat[3], pos)) continue
yield model.append((name, sat[3], pos))
finally: finally:
self._satellite_label.show() self._satellite_label.show()
def on_satellite_selection(self, view, path, column): def on_satellite_selection(self, view, path, column):
model = view.get_model()
self._url_entry.set_text(model.get(model.get_iter(path), 1)[0])
def on_load_providers(self, item):
self.on_info_bar_close() self.on_info_bar_close()
model = self._providers_view.get_model() model = self._providers_view.get_model()
model.clear() model.clear()
self.get_providers(model) self._satellite_label.set_visible(False)
self.get_providers(view.get_model()[path][1], model)
@run_task @run_task
def get_providers(self, model): def get_providers(self, url, model):
providers = parse_providers(self._url_entry.get_text()) if self._download_src is self.DownloadSource.LYNG_SAT:
if providers: providers = parse_providers(url)
self.append_providers(providers, model) elif self._download_src is self.DownloadSource.PICON_CZ:
providers = self._picon_cz_downloader.get_sat_providers(url)
else:
return
self.append_providers(providers or [], model)
@run_idle @run_idle
def append_providers(self, providers, model): def append_providers(self, providers, model):
for p in providers: if self._download_src is self.DownloadSource.LYNG_SAT:
model.append((self.get_pixbuf(p[0]) if p[0] else TV_ICON, *p[1:])) for p in providers:
self.update_receive_button_state() model.append(p._replace(logo=self.get_pixbuf(p.logo) if p.logo else TV_ICON))
elif self._download_src is self.DownloadSource.PICON_CZ:
for p in providers:
logo_data = self._picon_cz_downloader.get_logo_data(p.ssid)
model.append(p._replace(logo=self.get_pixbuf(logo_data) if logo_data else TV_ICON))
def get_pixbuf(self, img_data): self.update_receive_button_state()
GLib.idle_add(self._satellite_label.set_visible, True)
def get_pixbuf(self, img_data, w=48, h=32):
if img_data: if img_data:
f = Gio.MemoryInputStream.new_from_data(img_data) f = Gio.MemoryInputStream.new_from_data(img_data)
return GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, 48, 32, True, None) return GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, w, h, True, None)
def on_receive(self, item): def on_receive(self, item):
self._cancel_button.show()
self.start_download()
@run_task
def start_download(self):
if self._is_downloading: if self._is_downloading:
self.show_dialog("The task is already running!", DialogType.ERROR) self.show_dialog("The task is already running!", DialogType.ERROR)
return return
providers = self.get_selected_providers()
if self._download_src is self.DownloadSource.PICON_CZ and len(providers) > 1:
self.show_dialog("Please, select only one item!", DialogType.ERROR)
return
self._cancel_button.show()
self.start_download(providers)
@run_task
def start_download(self, providers):
self._is_downloading = True self._is_downloading = True
GLib.idle_add(self._expander.set_expanded, True) GLib.idle_add(self._expander.set_expanded, True)
providers = self.get_selected_providers()
for prv in providers: for prv in providers:
if not self._POS_PATTERN.match(prv[2]): if self._download_src is self.DownloadSource.LYNG_SAT and not self._POS_PATTERN.match(prv[2]):
self.show_info_message( self.show_info_message(
get_message("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR) get_message("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR)
scroll_to(prv.path, self._providers_view) scroll_to(prv.path, self._providers_view)
@@ -538,33 +665,13 @@ class PiconsDialog:
try: try:
picons_path = self._picons_dir_entry.get_text() picons_path = self._picons_dir_entry.get_text()
os.makedirs(os.path.dirname(picons_path), exist_ok=True) os.makedirs(os.path.dirname(picons_path), exist_ok=True)
picons = []
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO) self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
providers = (Provider(*p) for p in providers)
import concurrent.futures if self._download_src is self.DownloadSource.LYNG_SAT:
self.get_picons_for_lyngsat(picons_path, providers)
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: elif self._download_src is self.DownloadSource.PICON_CZ:
# Getting links to picons. self.get_picons_for_picon_cz(picons_path, providers)
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, 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: if not self._is_downloading:
return return
@@ -577,6 +684,50 @@ class PiconsDialog:
GLib.idle_add(self._cancel_button.hide) GLib.idle_add(self._cancel_button.hide)
self._is_downloading = False self._is_downloading = False
def get_picons_for_lyngsat(self, path, providers):
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
picons = []
# Getting links to picons.
futures = {executor.submit(self.process_provider, p, 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, 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)
def get_picons_for_picon_cz(self, path, providers):
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
futures = {executor.submit(self._picon_cz_downloader.download, p, path): p for p in providers}
for future in concurrent.futures.as_completed(futures):
if not self._is_downloading:
executor.shutdown()
return
try:
future.result()
except PiconsError as e:
self.append_output("Error: {}\n".format(str(e)))
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)
def process_provider(self, prv, picons_path): def process_provider(self, prv, picons_path):
self.append_output("Getting links to picons for: {}.\n".format(prv.name)) 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()) return PiconsParser.parse(prv, picons_path, self._picon_ids, self.get_picons_format())
@@ -781,7 +932,7 @@ class PiconsDialog:
def on_url_changed(self, entry): def on_url_changed(self, entry):
suit = self._PATTERN.search(entry.get_text()) suit = self._PATTERN.search(entry.get_text())
entry.set_name("GtkEntry" if suit else "digit-entry") entry.set_name("GtkEntry" if suit else "digit-entry")
self._load_providers_button.set_sensitive(suit if suit else False) self._download_source_button.set_sensitive(suit if suit else False)
def on_position_edited(self, render, path, value): def on_position_edited(self, render, path, value):
model = self._providers_view.get_model() model = self._providers_view.get_model()
@@ -791,7 +942,7 @@ class PiconsDialog:
def on_visible_page(self, stack: Gtk.Stack, param): def on_visible_page(self, stack: Gtk.Stack, param):
name = stack.get_visible_child_name() name = stack.get_visible_child_name()
self._convert_button.set_visible(name == "converter") self._convert_button.set_visible(name == "converter")
self._load_providers_button.set_visible(name == "downloader") self._download_source_button.set_visible(name == "downloader")
is_explorer = name == "explorer" is_explorer = name == "explorer"
self._filter_button.set_visible(is_explorer) self._filter_button.set_visible(is_explorer)
if is_explorer: if is_explorer: