Files
DemonEditor/app/ui/iptv.py

1355 lines
58 KiB
Python
Raw Normal View History

2021-05-17 00:07:44 +03:00
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
2025-01-04 00:58:42 +03:00
# Copyright (c) 2018-2025 Dmitriy Yefremov
2021-05-17 00:07:44 +03:00
#
# 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
#
2019-06-24 00:36:54 +03:00
import concurrent.futures
import os
import re
2019-01-27 23:20:07 +03:00
import urllib
from datetime import date
from itertools import groupby, chain
from urllib.error import HTTPError
2020-06-10 11:10:41 +03:00
from urllib.parse import urlparse, unquote, quote
from urllib.request import Request, urlopen
import requests
2021-01-31 16:27:35 +03:00
from gi.repository import GLib, Gio, GdkPixbuf
2021-01-31 16:27:35 +03:00
from app.commons import run_idle, run_task, log
from app.eparser.ecommons import BqServiceType, BouquetService, Service
2021-01-31 16:27:35 +03:00
from app.eparser.iptv import (NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT,
parse_m3u, PICON_FORMAT)
2023-02-18 11:30:06 +03:00
from app.settings import SettingsType
from app.tools.yt import YouTubeException, YouTube
2024-01-21 23:03:56 +03:00
from app.ui.dialogs import Action, show_dialog, DialogType, translate, get_builder, BaseDialog
2025-01-04 00:58:42 +03:00
from app.ui.epg.epg import EpgCache
from app.ui.main_helper import get_iptv_url, on_popup_menu, get_picon_pixbuf, show_info_bar_message, gen_bouquet_name
2023-02-18 11:30:06 +03:00
from app.ui.uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, IPTV_ICON, Column, KeyboardKey, get_yt_icon, HeaderBar)
2018-03-11 21:52:10 +03:00
_DIGIT_ENTRY_NAME = "digit-entry"
_ENIGMA2_REFERENCE = "{}:{}:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0"
2019-05-09 14:48:29 +03:00
_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
_UI_PATH = f"{UI_RESOURCES_PATH}iptv.glade"
_CSS_PATH = f"{UI_RESOURCES_PATH}style.css"
_URL_PREFIXES = {"YT-DLP": "YT-DLP://", "YT-DL": "YT-DL://", "STREAMLINK": "streamlink://", "No": None}
def is_data_correct(elems):
for elem in elems:
if elem.get_name() == _DIGIT_ENTRY_NAME:
return False
return True
2018-03-11 21:52:10 +03:00
2019-04-14 00:03:52 +03:00
def get_stream_type(box):
active = box.get_active()
if active == 0:
return StreamType.DVB_TS.value
elif active == 1:
return StreamType.NONE_TS.value
elif active == 2:
return StreamType.NONE_REC_1.value
2020-06-10 11:10:41 +03:00
elif active == 3:
return StreamType.NONE_REC_2.value
2021-01-11 12:30:44 +03:00
elif active == 4:
return StreamType.E_SERVICE_URI.value
return StreamType.E_SERVICE_HLS.value
2019-04-14 00:03:52 +03:00
2018-03-11 21:52:10 +03:00
class IptvDialog:
2018-04-01 09:39:14 +03:00
2022-02-21 12:22:44 +03:00
def __init__(self, app, view, bouquet=None, service=None, action=Action.ADD):
handlers = {"on_response": self.on_response,
"on_entry_changed": self.on_entry_changed,
2018-04-01 09:39:14 +03:00
"on_url_changed": self.on_url_changed,
"on_url_paste": self.on_url_paste,
2018-03-15 23:10:22 +03:00
"on_save": self.on_save,
"on_stream_type_changed": self.on_stream_type_changed,
2019-08-18 00:06:44 +03:00
"on_yt_quality_changed": self.on_yt_quality_changed,
"on_info_bar_close": self.on_info_bar_close}
2018-03-15 23:10:22 +03:00
2022-02-21 12:22:44 +03:00
self._app = app
2020-06-10 11:10:41 +03:00
self._action = action
2022-02-21 12:22:44 +03:00
self._settings = app.app_settings
self._s_type = self._settings.setting_type
2020-06-10 11:10:41 +03:00
self._bouquet = bouquet
self._yt_links = None
self._yt_dl = None
self._inserted_url = False
2020-06-10 11:10:41 +03:00
2021-04-28 14:12:59 +03:00
builder = get_builder(_UI_PATH, handlers, use_str=True,
objects=("iptv_dialog", "stream_type_liststore", "yt_quality_liststore"))
self._dialog = builder.get_object("iptv_dialog")
2022-02-21 12:22:44 +03:00
self._dialog.set_transient_for(app.app_window)
2018-03-11 21:52:10 +03:00
self._name_entry = builder.get_object("name_entry")
2018-03-12 22:47:43 +03:00
self._description_entry = builder.get_object("description_entry")
2018-03-11 21:52:10 +03:00
self._url_entry = builder.get_object("url_entry")
2023-07-13 22:19:43 +03:00
self._reference_label = builder.get_object("iptv_reference_label")
2018-03-11 21:52:10 +03:00
self._srv_type_entry = builder.get_object("srv_type_entry")
2021-10-23 00:18:51 +03:00
self._srv_id_entry = builder.get_object("srv_id_entry")
2018-03-11 21:52:10 +03:00
self._sid_entry = builder.get_object("sid_entry")
self._tr_id_entry = builder.get_object("tr_id_entry")
self._net_id_entry = builder.get_object("net_id_entry")
self._namespace_entry = builder.get_object("namespace_entry")
self._stream_type_combobox = builder.get_object("stream_type_combobox")
self._add_button = builder.get_object("iptv_dialog_add_button")
self._save_button = builder.get_object("iptv_dialog_save_button")
2018-03-12 22:47:43 +03:00
self._stream_type_combobox = builder.get_object("stream_type_combobox")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
2019-08-18 00:06:44 +03:00
self._yt_quality_box = builder.get_object("yt_iptv_quality_combobox")
2023-03-27 00:01:08 +03:00
self._url_prefix_box = builder.get_object("iptv_url_prefix_box")
self._url_prefix_combobox = builder.get_object("iptv_url_prefix_combobox")
self._model, self._paths = view.get_selection().get_selected_rows()
# Style.
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(_CSS_PATH)
2021-10-23 00:18:51 +03:00
self._digit_elems = (self._srv_id_entry, self._srv_type_entry, self._sid_entry, self._tr_id_entry,
self._net_id_entry, self._namespace_entry)
for el in self._digit_elems:
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
2020-06-10 11:10:41 +03:00
if self._s_type is SettingsType.NEUTRINO_MP:
2018-10-15 12:36:03 +03:00
builder.get_object("iptv_dialog_ts_data_frame").set_visible(False)
builder.get_object("iptv_type_label").set_visible(False)
2023-07-06 17:19:15 +03:00
builder.get_object("iptv_ref_box").set_visible(False)
self._stream_type_combobox.set_visible(False)
2018-03-15 23:10:22 +03:00
else:
self._description_entry.set_visible(False)
builder.get_object("iptv_description_label").set_visible(False)
2023-03-30 00:13:25 +03:00
[self._url_prefix_combobox.append(v, k) for k, v in _URL_PREFIXES.items()]
2023-03-27 00:01:08 +03:00
self._url_prefix_combobox.set_active(0)
2018-03-11 21:52:10 +03:00
if self._action is Action.ADD:
self._save_button.set_visible(False)
self._add_button.set_visible(True)
2020-06-10 11:10:41 +03:00
if self._s_type is SettingsType.ENIGMA_2:
self.update_reference_entry()
2019-06-15 23:50:42 +03:00
self._stream_type_combobox.set_active(1)
2018-03-11 21:52:10 +03:00
elif self._action is Action.EDIT:
2022-02-21 12:22:44 +03:00
self._current_srv = service
self.init_data(self._current_srv)
2018-03-11 21:52:10 +03:00
def show(self):
self._dialog.run()
2018-09-07 23:42:59 +03:00
def on_response(self, dialog, response):
2020-07-24 11:37:27 +03:00
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
self._dialog.destroy()
2018-03-11 21:52:10 +03:00
def on_save(self, item):
2019-06-15 23:50:42 +03:00
if self._action is Action.ADD:
self.on_url_changed(self._url_entry)
if not is_data_correct(self._digit_elems) or self._url_entry.get_name() == _DIGIT_ENTRY_NAME:
2024-01-20 19:04:20 +03:00
self.show_info_message("Error. Verify the data!", Gtk.MessageType.ERROR)
2018-04-01 09:39:14 +03:00
return
url = self._url_entry.get_text()
2023-04-02 12:42:59 +03:00
if all((self._url_prefix_box.get_visible(),
self._url_prefix_combobox.get_active_id(),
url.count("http") > 1 or urlparse(url).scheme.upper() in _URL_PREFIXES)):
2024-01-20 19:04:20 +03:00
self.show_info_message("Invalid prefix for the given URL!", Gtk.MessageType.ERROR)
2023-04-02 12:42:59 +03:00
return
2020-07-24 11:37:27 +03:00
if show_dialog(DialogType.QUESTION, self._dialog) in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
2020-06-10 11:10:41 +03:00
self.save_enigma2_data() if self._s_type is SettingsType.ENIGMA_2 else self.save_neutrino_data()
self._dialog.destroy()
2018-03-11 21:52:10 +03:00
2018-03-12 22:47:43 +03:00
def init_data(self, srv):
2022-02-21 12:22:44 +03:00
fav_id = srv.fav_id
self._name_entry.set_text(srv.service)
2020-06-10 11:10:41 +03:00
self.init_enigma2_data(fav_id) if self._s_type is SettingsType.ENIGMA_2 else self.init_neutrino_data(fav_id)
def init_enigma2_data(self, fav_id):
2019-01-27 23:20:07 +03:00
data, sep, desc = fav_id.partition("#DESCRIPTION")
self._description_entry.set_text(desc.strip())
data = data.split(":")
2019-01-27 23:20:07 +03:00
if len(data) < 11:
return
2019-04-14 00:03:52 +03:00
s_type = data[0].strip()
try:
stream_type = StreamType(s_type)
if stream_type is StreamType.DVB_TS:
self._stream_type_combobox.set_active(0)
elif stream_type is StreamType.NONE_TS:
self._stream_type_combobox.set_active(1)
elif stream_type is StreamType.NONE_REC_1:
self._stream_type_combobox.set_active(2)
elif stream_type is StreamType.NONE_REC_2:
self._stream_type_combobox.set_active(3)
2020-06-10 11:10:41 +03:00
elif stream_type is StreamType.E_SERVICE_URI:
self._stream_type_combobox.set_active(4)
2021-01-11 12:30:44 +03:00
elif stream_type is StreamType.E_SERVICE_HLS:
self._stream_type_combobox.set_active(5)
2019-04-14 00:03:52 +03:00
except ValueError:
self.show_info_message(f"Unknown stream type {s_type}", Gtk.MessageType.ERROR)
2019-04-14 00:03:52 +03:00
2021-10-23 00:18:51 +03:00
self._srv_id_entry.set_text(data[1])
self._srv_type_entry.set_text(str(int(data[2], 16)))
2018-04-10 16:03:36 +03:00
self._sid_entry.set_text(str(int(data[3], 16)))
self._tr_id_entry.set_text(str(int(data[4], 16)))
self._net_id_entry.set_text(str(int(data[5], 16)))
self._namespace_entry.set_text(str(int(data[6], 16)))
2023-03-27 00:01:08 +03:00
# URL.
url = unquote(data[10].strip())
sch = urlparse(url).scheme.upper()
if YouTube.get_yt_id(url) and sch in _URL_PREFIXES:
2023-03-30 00:13:25 +03:00
active_prefix = _URL_PREFIXES.get(sch)
url = re.sub(active_prefix, "", url, 1, re.IGNORECASE)
2023-03-27 00:01:08 +03:00
self._url_prefix_combobox.set_active_id(active_prefix)
2023-04-02 12:42:59 +03:00
else:
self._url_prefix_combobox.set_active(len(_URL_PREFIXES) - 1)
2023-03-27 00:01:08 +03:00
self._url_entry.set_text(url)
2020-06-10 11:10:41 +03:00
self.update_reference_entry()
def init_neutrino_data(self, fav_id):
data = fav_id.split("::")
self._url_entry.set_text(data[0])
self._description_entry.set_text(data[1])
2018-03-12 22:47:43 +03:00
2020-06-10 11:10:41 +03:00
def update_reference_entry(self):
2020-07-24 11:37:27 +03:00
if self._s_type is SettingsType.ENIGMA_2 and is_data_correct(self._digit_elems):
2023-07-13 22:19:43 +03:00
self._reference_label.set_text(_ENIGMA2_REFERENCE.format(self.get_type(),
2021-10-23 00:18:51 +03:00
self._srv_id_entry.get_text(),
int(self._srv_type_entry.get_text()),
int(self._sid_entry.get_text()),
int(self._tr_id_entry.get_text()),
int(self._net_id_entry.get_text()),
int(self._namespace_entry.get_text())))
2018-03-12 22:47:43 +03:00
def get_type(self):
2019-04-14 00:03:52 +03:00
return get_stream_type(self._stream_type_combobox)
2018-03-12 22:47:43 +03:00
def on_entry_changed(self, entry):
entry.set_name(_DIGIT_ENTRY_NAME if _PATTERN.search(entry.get_text()) else "GtkEntry")
self.update_reference_entry()
2018-04-01 09:39:14 +03:00
def on_url_changed(self, entry):
2019-06-15 23:50:42 +03:00
url_str = entry.get_text()
url = urlparse(url_str)
2021-01-11 12:30:44 +03:00
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
2020-06-10 11:10:41 +03:00
entry.set_name("GtkEntry" if cond else _DIGIT_ENTRY_NAME)
yt_id = YouTube.get_yt_id(url_str)
if yt_id:
2019-06-15 23:50:42 +03:00
entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
text = "Found a link to the YouTube resource!\nTry to get a direct link to the video?"
2023-04-02 12:42:59 +03:00
if self._inserted_url and url_str.count("http") == 1:
if show_dialog(DialogType.QUESTION, self._dialog, text=text) == Gtk.ResponseType.OK:
entry.set_sensitive(False)
gen = self.set_yt_url(entry, yt_id)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
2023-04-02 12:49:00 +03:00
else:
self._url_prefix_box.set_visible(self._s_type is SettingsType.ENIGMA_2)
else:
self._url_prefix_box.set_visible(self._s_type is SettingsType.ENIGMA_2)
self._inserted_url = False
elif YouTube.is_yt_video_link(url_str):
entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
2019-06-15 23:50:42 +03:00
else:
entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
2023-03-27 00:01:08 +03:00
self._url_prefix_box.set_visible(False)
2018-04-01 09:39:14 +03:00
def on_url_paste(self, entry):
self._inserted_url = True
2023-03-26 16:22:12 +03:00
self._yt_quality_box.set_visible(False)
def set_yt_url(self, entry, video_id):
try:
2020-06-13 20:57:37 +03:00
if not self._yt_dl:
def callback(message, error=True):
msg_type = Gtk.MessageType.ERROR if error else Gtk.MessageType.INFO
self.show_info_message(message, msg_type)
self._yt_dl = YouTube.get_instance(self._settings, callback=callback)
yield True
links, title = self._yt_dl.get_yt_link(video_id, entry.get_text())
yield True
except urllib.error.URLError as e:
2023-05-13 13:31:42 +03:00
self.show_info_message(f"{translate('Getting link error:')} {e}", Gtk.MessageType.ERROR)
return
2020-06-13 20:57:37 +03:00
except YouTubeException as e:
2020-06-10 11:10:41 +03:00
self.show_info_message((str(e)), Gtk.MessageType.ERROR)
return
else:
if self._action is Action.ADD:
self._name_entry.set_text(title)
2019-08-18 00:06:44 +03:00
if links:
if len(links) > 1:
self._yt_quality_box.set_visible(True)
entry.set_text(links[sorted(links, key=lambda x: int(x.rstrip("p")), reverse=True)[0]])
self._yt_links = links
else:
2023-05-13 13:31:42 +03:00
msg = f"{translate('Getting link error:')} No link received for id: {video_id}"
self.show_info_message(msg, Gtk.MessageType.ERROR)
finally:
entry.set_sensitive(True)
yield True
2018-03-15 23:10:22 +03:00
def on_stream_type_changed(self, item):
2021-01-11 12:30:44 +03:00
if self.get_type() in (StreamType.E_SERVICE_URI.value, StreamType.E_SERVICE_HLS.value):
2020-06-10 11:10:41 +03:00
self.show_info_message("DreamOS only!", Gtk.MessageType.WARNING)
self.update_reference_entry()
2018-03-15 23:10:22 +03:00
2019-08-18 00:06:44 +03:00
def on_yt_quality_changed(self, box):
2023-03-26 16:22:12 +03:00
if not self._yt_links:
return
2019-08-18 00:06:44 +03:00
model = box.get_model()
active = model.get_value(box.get_active_iter(), 0)
2023-03-26 16:22:12 +03:00
if active in self._yt_links:
2019-08-18 00:06:44 +03:00
self._url_entry.set_text(self._yt_links[active])
2023-03-26 16:22:12 +03:00
else:
self._url_entry.set_text(self._yt_links.get(max(self._yt_links, default=None), ""))
2019-08-18 00:06:44 +03:00
def save_enigma2_data(self):
2018-03-15 23:10:22 +03:00
name = self._name_entry.get_text().strip()
2023-03-27 00:01:08 +03:00
if self._url_prefix_box.get_visible():
2023-04-02 12:42:59 +03:00
prefix = self._url_prefix_combobox.get_active_id()
url = self._url_entry.get_text().replace(':', '%3A', 1 if prefix else -1)
url = f"{quote(prefix) if prefix else ''}{url}"
2023-03-27 00:01:08 +03:00
else:
url = quote(self._url_entry.get_text())
2018-03-15 23:10:22 +03:00
fav_id = ENIGMA2_FAV_ID_FORMAT.format(self.get_type(),
2021-10-23 00:18:51 +03:00
self._srv_id_entry.get_text(),
int(self._srv_type_entry.get_text()),
2018-04-10 16:03:36 +03:00
int(self._sid_entry.get_text()),
int(self._tr_id_entry.get_text()),
int(self._net_id_entry.get_text()),
int(self._namespace_entry.get_text()),
2023-03-27 00:01:08 +03:00
url, name, name)
2022-02-21 12:22:44 +03:00
2018-03-15 23:10:22 +03:00
self.update_bouquet_data(name, fav_id)
def save_neutrino_data(self):
2018-03-16 00:10:33 +03:00
if self._action is Action.EDIT:
2022-02-21 12:22:44 +03:00
id_data = self._current_srv.fav_id.split("::")
2018-03-16 00:10:33 +03:00
else:
id_data = ["", "", "0", None, None, None, None, "", "", "1"]
2018-03-15 23:10:22 +03:00
id_data[0] = self._url_entry.get_text()
id_data[1] = self._description_entry.get_text()
self.update_bouquet_data(self._name_entry.get_text(), NEUTRINO_FAV_ID_FORMAT.format(*id_data))
self._dialog.destroy()
2018-03-15 23:10:22 +03:00
def update_bouquet_data(self, name, fav_id):
2023-07-13 22:19:43 +03:00
picon_id = f"{self._reference_label.get_text().replace(':', '_')}.png"
2022-02-21 12:22:44 +03:00
if self._action is Action.EDIT:
2022-02-21 12:22:44 +03:00
services = self._app.current_services
old_srv = services.pop(self._current_srv.fav_id)
new_service = old_srv._replace(service=name, fav_id=fav_id, picon_id=picon_id)
services[fav_id] = new_service
self._app.emit("iptv-service-edited", {self._current_srv.fav_id: (old_srv, new_service)})
2018-03-16 00:10:33 +03:00
else:
2022-02-21 12:22:44 +03:00
aggr = [None] * 8
2018-03-16 00:10:33 +03:00
s_type = BqServiceType.IPTV.name
2019-03-14 13:43:13 +03:00
srv = (None, None, name, None, None, s_type, None, fav_id, *aggr[0:3])
2018-03-16 00:10:33 +03:00
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)
2022-02-21 12:22:44 +03:00
service = Service(None, None, IPTV_ICON, name, *aggr[0:3], s_type, None, picon_id, *aggr, fav_id, None)
self._app.current_services[fav_id] = service
self._app.emit("iptv-service-added", (service,))
@run_idle
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_idle
def show_info_message(self, text, message_type):
2024-01-20 19:04:20 +03:00
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
2018-03-11 21:52:10 +03:00
class SearchUnavailableDialog:
2020-06-10 11:10:41 +03:00
def __init__(self, transient, model, fav_bouquet, iptv_rows, s_type):
2018-11-12 13:43:05 +03:00
handlers = {"on_response": self.on_response}
2021-04-28 14:12:59 +03:00
builder = get_builder(UI_RESOURCES_PATH + "iptv.glade", handlers,
objects=("search_unavailable_streams_dialog",))
self._dialog = builder.get_object("search_unavailable_streams_dialog")
self._dialog.set_transient_for(transient)
self._model = model
self._counter_label = builder.get_object("streams_rows_counter_label")
self._level_bar = builder.get_object("unavailable_streams_level_bar")
self._bouquet = fav_bouquet
2020-06-10 11:10:41 +03:00
self._s_type = s_type
self._iptv_rows = iptv_rows
self._counter = -1
self._max_rows = len(self._iptv_rows)
self._level_bar.set_max_value(self._max_rows)
self._download_task = True
self._to_delete = []
self.update_counter()
self.do_search()
@run_task
def do_search(self):
2022-03-20 01:02:19 +03:00
import ssl
import certifi
context = ssl.create_default_context(cafile=certifi.where())
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
2022-03-20 01:02:19 +03:00
futures = {executor.submit(self.get_unavailable, row, context): row for row in self._iptv_rows}
for future in concurrent.futures.as_completed(futures):
if not self._download_task:
executor.shutdown()
return
future.result()
self._download_task = False
self.on_close()
2022-03-20 01:02:19 +03:00
def get_unavailable(self, row, context):
if not self._download_task:
return
try:
2020-06-10 11:10:41 +03:00
req = Request(get_iptv_url(row, self._s_type))
self.update_bar()
2022-03-20 01:02:19 +03:00
urlopen(req, context=context, timeout=2)
except HTTPError as e:
if e.code != 403:
self.append_data(row)
except Exception:
self.append_data(row)
def append_data(self, row):
self._to_delete.append(self._model.get_iter(row.path))
self.update_counter()
@run_idle
def update_bar(self):
self._max_rows -= 1
self._level_bar.set_value(self._max_rows)
@run_idle
def update_counter(self):
self._counter += 1
self._counter_label.set_text(str(self._counter))
def show(self):
response = self._dialog.run()
return self._to_delete if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT) else False
2018-11-12 13:43:05 +03:00
def on_response(self, dialog, response):
if response == Gtk.ResponseType.CANCEL:
self.on_close()
@run_idle
2018-11-12 13:43:05 +03:00
def on_close(self):
if self._download_task and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self._download_task = False
self._dialog.destroy()
2021-01-31 16:27:35 +03:00
class IptvListDialog:
""" Base class for working with iptv lists. """
2021-01-31 16:27:35 +03:00
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,
"on_stream_type_changed": self.on_stream_type_changed,
2021-10-23 00:18:51 +03:00
"on_default_id_toggled": self.on_default_id_toggled,
"on_default_type_toggled": self.on_default_type_toggled,
"on_auto_sid_toggled": self.on_auto_sid_toggled,
2018-08-22 00:09:52 +03:00
"on_default_tid_toggled": self.on_default_tid_toggled,
"on_default_nid_toggled": self.on_default_nid_toggled,
"on_default_namespace_toggled": self.on_default_namespace_toggled,
"on_reset_to_default": self.on_reset_to_default,
"on_entry_changed": self.on_entry_changed,
"on_info_bar_close": self.on_info_bar_close}
2020-06-10 11:10:41 +03:00
self._s_type = s_type
2021-04-28 14:12:59 +03:00
builder = get_builder(_UI_PATH, handlers, use_str=True,
objects=("iptv_list_configuration_dialog", "stream_type_liststore"))
self._dialog = builder.get_object("iptv_list_configuration_dialog")
self._dialog.set_transient_for(transient)
2021-01-31 16:27:35 +03:00
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")
2024-01-20 19:04:20 +03:00
self._message_label = builder.get_object("list_configuration_message_label")
self._reference_label = builder.get_object("reference_label")
self._stream_type_check_button = builder.get_object("stream_type_default_check_button")
2021-10-23 00:18:51 +03:00
self._id_default_check_button = builder.get_object("id_default_check_button")
self._type_check_button = builder.get_object("type_default_check_button")
self._sid_auto_check_button = builder.get_object("sid_auto_check_button")
self._tid_check_button = builder.get_object("tid_default_check_button")
self._nid_check_button = builder.get_object("nid_default_check_button")
self._namespace_check_button = builder.get_object("namespace_default_check_button")
self._stream_type_combobox = builder.get_object("stream_type_list_combobox")
2021-10-23 00:18:51 +03:00
self._list_srv_id_entry = builder.get_object("list_srv_id_entry")
self._list_srv_type_entry = builder.get_object("list_srv_type_entry")
self._list_sid_entry = builder.get_object("list_sid_entry")
2018-08-22 00:09:52 +03:00
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._apply_button = builder.get_object("list_configuration_apply_button")
2021-05-17 00:07:44 +03:00
self._cancel_button = builder.get_object("cancel_config_list_button")
2021-05-18 16:03:52 +03:00
self._ok_button = builder.get_object("list_configuration_ok_button")
2021-11-07 21:20:06 +03:00
self._ok_button.bind_property("visible", self._apply_button, "visible", 4)
self._ok_button.bind_property("visible", self._cancel_button, "visible", 4)
2021-01-31 16:27:35 +03:00
# Style
style_provider = Gtk.CssProvider()
style_provider.load_from_path(_CSS_PATH)
2021-10-23 00:18:51 +03:00
self._default_elems = (self._stream_type_check_button, self._id_default_check_button, self._type_check_button,
self._sid_auto_check_button, self._tid_check_button, self._nid_check_button,
self._namespace_check_button)
self._digit_elems = (self._list_srv_id_entry, 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:
2021-01-31 16:27:35 +03:00
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
def show(self):
2018-08-22 00:09:52 +03:00
self._dialog.run()
def on_response(self, dialog, response):
if response == Gtk.ResponseType.APPLY:
return True
self._dialog.destroy()
def on_stream_type_changed(self, box):
self.update_reference()
def on_stream_type_default_togged(self, button):
if button.get_active():
self._stream_type_combobox.set_active(1)
self._stream_type_combobox.set_sensitive(not button.get_active())
2021-10-23 00:18:51 +03:00
def on_default_id_toggled(self, button):
self.set_default(button, self._list_srv_id_entry, "0")
def on_default_type_toggled(self, button):
2021-10-23 00:18:51 +03:00
self.set_default(button, self._list_srv_type_entry, "1")
def on_auto_sid_toggled(self, button):
2021-10-23 00:18:51 +03:00
self.set_default(button, self._list_sid_entry, "0")
2018-08-22 00:09:52 +03:00
def on_default_tid_toggled(self, button):
2021-10-23 00:18:51 +03:00
self.set_default(button, self._list_tid_entry, "0")
2018-08-22 00:09:52 +03:00
def on_default_nid_toggled(self, button):
2021-10-23 00:18:51 +03:00
self.set_default(button, self._list_nid_entry, "0")
def on_default_namespace_toggled(self, button):
2021-10-23 00:18:51 +03:00
self.set_default(button, self._list_namespace_entry, "0")
def set_default(self, button, entry, value):
if button.get_active():
2021-10-23 00:18:51 +03:00
entry.set_text(value)
entry.set_sensitive(not button.get_active())
@run_idle
2021-01-31 16:27:35 +03:00
def on_reset_to_default(self, item):
self._stream_type_combobox.set_active(1)
self._list_srv_type_entry.set_text("1")
2021-01-31 16:27:35 +03:00
for el in self._digit_elems[1:]:
el.set_text("0")
2021-01-31 16:27:35 +03:00
for el in self._default_elems:
el.set_active(True)
2024-01-20 19:04:20 +03:00
@run_idle
def show_info_message(self, text, message_type=Gtk.MessageType.INFO):
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
2021-01-31 16:27:35 +03:00
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):
2021-10-23 00:18:51 +03:00
return any(el.get_text() == "0" for el in self._digit_elems[3:])
2021-01-31 16:27:35 +03:00
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):
2024-01-20 19:04:20 +03:00
self.show_info_message("Error. Verify the data!", Gtk.MessageType.ERROR)
return
2020-06-10 11:10:41 +03:00
if self._s_type is SettingsType.ENIGMA_2:
2021-10-23 00:18:51 +03:00
id_default = self._id_default_check_button.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()
2021-11-07 21:20:06 +03:00
all_default = self.is_all_data_default()
2021-10-23 00:18:51 +03:00
st_type = get_stream_type(self._stream_type_combobox)
2021-11-07 21:20:06 +03:00
s_id = "0" if id_default else self._list_srv_id_entry.get_text()
srv_type = int("1" if type_default else self._list_srv_type_entry.get_text())
2021-11-07 21:20:06 +03:00
sid = "0" if sid_auto else self._list_sid_entry.get_text()
tid = "0" if tid_default else f"{int(self._list_tid_entry.get_text()):X}"
nid = "0" if nid_default else f"{int(self._list_nid_entry.get_text()):X}"
namespace = "0" if namespace_default else f"{int(self._list_namespace_entry.get_text()):X}"
params = [int(el.get_text()) for el in self._digit_elems[2:]]
for index, row in enumerate(self._rows):
fav_id = row[Column.FAV_ID]
data, sep, desc = fav_id.partition("http")
data = data.split(":")
2021-11-07 21:20:06 +03:00
if all_default:
2021-10-23 00:18:51 +03:00
data[1], data[2], data[3], data[4], data[5], data[6] = "010000"
else:
data[0], data[1], data[4], data[5], data[6] = st_type, s_id, tid, nid, namespace
data[2] = f"{srv_type:X}"
2021-11-07 21:20:06 +03:00
data[3] = f"{index:X}" if sid_auto else sid
if sid_auto:
params[0] = index
picon_id = PICON_FORMAT.format(st_type, int(s_id), srv_type, *params)
data = ":".join(data)
2021-11-07 21:20:06 +03:00
new_fav_id = f"{data}{sep}{desc}"
row[Column.FAV_ID] = new_fav_id
srv = self._services.pop(fav_id, None)
if srv:
self._services[new_fav_id] = srv._replace(fav_id=new_fav_id, picon_id=picon_id)
self._bouquet.clear()
list(map(lambda r: self._bouquet.append(r[Column.FAV_ID]), self._fav_model))
2024-01-20 19:04:20 +03:00
self.show_info_message("Done!", Gtk.MessageType.INFO)
2021-11-07 21:20:06 +03:00
self._ok_button.set_visible(True)
2021-01-31 16:27:35 +03:00
class M3uImportDialog(IptvListDialog):
""" Import dialog for *.m3u* playlists. """
def __init__(self, transient, s_type, m3_path, app):
2021-01-31 16:27:35 +03:00
super().__init__(transient, s_type)
self._app = app
2022-01-26 23:07:41 +03:00
self._picons = app.picons
2021-08-30 15:04:15 +03:00
self._pic_path = app._settings.profile_picons_path
2021-01-31 16:27:35 +03:00
self._services = None
2024-01-20 15:32:45 +03:00
self._epg_src = None
2021-01-31 16:27:35 +03:00
self._url_count = 0
self._errors_count = 0
2021-01-31 16:27:35 +03:00
self._max_count = 0
self._is_download = False
self._cancellable = Gio.Cancellable()
2023-05-13 13:31:42 +03:00
self._dialog.set_title(translate("Playlist import"))
self._dialog.connect("delete-event", self.on_close)
2023-05-13 13:31:42 +03:00
self._apply_button.set_label(translate("Import"))
2024-01-19 23:40:45 +03:00
# Extra box.
builder = get_builder(f"{UI_RESOURCES_PATH}m3u.glade", use_str=True, objects=("import_m3u_box",))
self._info_label = builder.get_object("info_label")
self._progress_bar = builder.get_object("progress_bar")
self._spinner = builder.get_object("spinner")
2021-01-31 16:27:35 +03:00
self._spinner.bind_property("active", self._start_values_grid, "sensitive", 4)
2024-01-19 23:40:45 +03:00
self._picon_switch = builder.get_object("picon_switch")
self._picon_box = builder.get_object("picon_box")
2024-01-28 13:58:29 +03:00
# Type import buttons.
self._current_bq_button = builder.get_object("current_bq_button")
self._single_bq_button = builder.get_object("single_bq_button")
self._group_bq_button = builder.get_object("group_bq_button")
self._sub_bq_button = builder.get_object("sub_bq_button")
# EPG src.
self._epg_links_button = builder.get_object("epg_links_box")
self._add_epg_src_switch = builder.get_object("add_epg_src_switch")
m3u_box = builder.get_object("import_m3u_box")
if s_type is SettingsType.ENIGMA_2:
self._data_box.add(m3u_box)
else:
self._data_box.set_visible(False)
self._group_bq_button.set_sensitive(False)
2024-01-28 13:58:29 +03:00
self._sub_bq_button.set_sensitive(False)
m3u_box.set_margin_start(5)
m3u_box.set_margin_end(5)
area = self._dialog.get_content_area()
area.pack_start(m3u_box, True, True, 0)
area.reorder_child(m3u_box, 0)
2021-01-31 16:27:35 +03:00
self.get_m3u(m3_path, s_type)
2021-01-31 16:27:35 +03:00
@run_task
def get_m3u(self, path, s_type):
try:
2024-01-28 13:58:29 +03:00
GLib.idle_add(self._spinner.start)
2024-01-20 15:32:45 +03:00
self._epg_src, self._services = parse_m3u(path, s_type)
2021-01-31 16:27:35 +03:00
for s in self._services:
if s.picon:
GLib.idle_add(self._picon_box.set_sensitive, True)
break
finally:
2024-01-28 13:58:29 +03:00
self.update_info()
@run_idle
def update_info(self):
msg = f"{translate('Streams detected:')} {len(self._services) if self._services else 0}."
self._info_label.set_text(msg)
self._spinner.stop()
if self._epg_src:
self._epg_links_button.set_visible(True)
[self._epg_links_button.append(u, u) for u in self._epg_src]
self._epg_links_button.set_active(0)
2021-01-31 16:27:35 +03:00
def on_apply(self, item):
if self._current_bq_button.get_active() and not self._app.current_bouquet:
2024-01-20 19:04:20 +03:00
self.show_info_message("Error. No bouquet is selected!", Gtk.MessageType.ERROR)
return
2021-01-31 16:27:35 +03:00
if not is_data_correct(self._digit_elems):
2024-01-20 19:04:20 +03:00
self.show_info_message("Error. Verify the data!", Gtk.MessageType.ERROR)
2021-01-31 16:27:35 +03:00
return
picons = {}
services = self._services
2025-01-04 00:58:42 +03:00
if self._app.app_settings.enable_epg_name_cache:
EpgCache.update_name_cache(self._app.app_settings.default_data_path, {s[3]: s[0] for s in services if s[0]})
2021-01-31 16:27:35 +03:00
if not self.is_all_data_default():
services = []
params = [int(el.get_text()) for el in self._digit_elems]
2021-10-23 00:18:51 +03:00
s_id = params[0]
s_type = params[1]
params = params[2:]
st_type = get_stream_type(self._stream_type_combobox)
2021-11-07 21:20:06 +03:00
sid_auto = self._sid_auto_check_button.get_active()
sid = 0 if sid_auto else int(self._list_sid_entry.get_text())
2021-01-31 16:27:35 +03:00
for i, s in enumerate(self._services, start=params[0]):
# Skipping markers.
if not s.data_id:
services.append(s)
continue
2021-11-07 21:20:06 +03:00
params[0] = i if sid_auto else sid
picon_id = PICON_FORMAT.format(st_type, s_id, s_type, *params)
2021-10-23 00:18:51 +03:00
fav_id = get_fav_id(s.data_id, s.service, self._s_type, params, st_type, s_id, s_type)
if s.picon:
picons[s.picon] = picon_id
2021-01-31 16:27:35 +03:00
services.append(s._replace(picon=None, picon_id=picon_id, data_id=None, fav_id=fav_id))
2021-01-31 16:27:35 +03:00
2024-01-28 13:58:29 +03:00
if self._add_epg_src_switch.get_active():
self.on_add_epg_source()
2024-01-19 23:40:45 +03:00
if self._picon_switch.get_active():
2021-01-31 16:27:35 +03:00
if self.is_default_values():
2024-01-20 19:04:20 +03:00
msg = "Set values for TID, NID and Namespace for correct naming of the picons!"
self.show_info_message(msg, Gtk.MessageType.ERROR)
2021-01-31 16:27:35 +03:00
return
self.download_picons(picons)
else:
2024-01-20 15:32:45 +03:00
self.on_apply_done()
2021-01-31 16:27:35 +03:00
self.import_services(services)
def import_services(self, services):
if self._current_bq_button.get_active():
self._app.append_imported_services(services)
return
s_type = self._app.app_settings.setting_type
model = self._app.bouquets_view.get_model()
if s_type is SettingsType.ENIGMA_2:
itr = model.get_iter_first()
else:
# We will use the 'FAV' section for Neutrino!
itr = model.get_iter(Gtk.TreePath.new_from_indices([1]))
bqs = self._app.current_bouquets
bq_type = model.get_value(itr, Column.BQ_TYPE)
def_bq_name = gen_bouquet_name(bqs, f"IPTV {date.today()} ", bq_type)
if self._single_bq_button.get_active():
self.append_bouquet(def_bq_name, bq_type, bqs, model, itr, services)
else:
# Sub-bouquets.
if self._sub_bq_button.get_active():
itr = self.append_bouquet(gen_bouquet_name(bqs, def_bq_name, bq_type), bq_type, bqs, model, itr, ())
# Generating groups with skipping markers.
m_name = BqServiceType.MARKER.value
def_bq_name = f"{def_bq_name} [No group]"
gr = self.get_services_groups(filter(lambda s: s.service_type != m_name, services), def_bq_name)
[self.append_bouquet(gen_bouquet_name(bqs, g, bq_type), bq_type, bqs, model, itr, s) for g, s in gr.items()]
def append_bouquet(self, bq_name, bq_type, bqs, model, itr, services):
""" Adds new bouquet and returns iter of appended row. """
cur_services = self._app.current_services
bqs[f"{bq_name}:{bq_type}"] = [s.fav_id for s in services]
cur_services.update({s.fav_id: s for s in services})
bq = (bq_name, None, None, bq_type)
return model.append(itr, bq)
def get_services_groups(self, services, def_gr_name="No group"):
def grouper(s):
return s.package or def_gr_name
return {k: list(v) for k, v in groupby(sorted(services, key=grouper), key=grouper)}
2021-01-31 16:27:35 +03:00
2024-01-28 13:58:29 +03:00
def on_add_epg_source(self):
active_src = self._epg_links_button.get_active_id()
settings = self._app.app_settings
sources = settings.epg_xml_sources
log(f"Adding an EPG source -> {active_src}")
if active_src not in set(sources):
sources.append(active_src)
settings.epg_xml_sources = sources
self._app.emit("epg-settings-changed", None)
else:
log(f"{translate('This URL already exists!')}")
2021-01-31 16:27:35 +03:00
@run_task
def download_picons(self, picons):
self._is_download = True
os.makedirs(os.path.dirname(self._pic_path), exist_ok=True)
2021-01-31 16:27:35 +03:00
GLib.idle_add(self._apply_button.set_sensitive, False)
GLib.idle_add(self._progress_bar.set_visible, True)
self._errors_count = 0
2021-01-31 16:27:35 +03:00
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)
2021-01-31 16:27:35 +03:00
@run_idle
def on_picon_load_done(self, data, user_data):
2021-01-31 16:27:35 +03:00
try:
2021-11-07 21:20:06 +03:00
self._info_label.set_text(f"Processing: {user_data}")
f = Gio.MemoryInputStream.new_from_data(data)
pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, 220, 132, False, self._cancellable)
2021-11-07 21:20:06 +03:00
path = f"{self._pic_path}{user_data}"
pixbuf.savev(path, "png", [], [])
self._picons[user_data] = get_picon_pixbuf(path)
2021-01-31 16:27:35 +03:00
except GLib.GError as e:
self.update_progress(1)
2021-01-31 16:27:35 +03:00
if e.code != Gio.IOErrorEnum.CANCELLED:
2021-11-07 21:20:06 +03:00
log(f"Loading picon [{user_data}] data error: {e}")
else:
2021-01-31 16:27:35 +03:00
self.update_progress()
@run_idle
def update_progress(self, error=0):
self._errors_count += error
2021-01-31 16:27:35 +03:00
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)
2021-11-07 21:20:06 +03:00
self._info_label.set_text(f"Errors: {self._errors_count}.")
self._is_download = False
2021-01-31 16:27:35 +03:00
gen = self.update_fav_model()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def update_fav_model(self):
2021-11-07 21:20:06 +03:00
services = self._app.current_services
2022-01-26 23:07:41 +03:00
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
2021-05-17 00:07:44 +03:00
2024-01-20 15:32:45 +03:00
self.on_apply_done()
yield True
@run_idle
def on_apply_done(self):
2024-01-20 19:04:20 +03:00
self.show_info_message("Done!", Gtk.MessageType.INFO)
2021-05-17 00:07:44 +03:00
self._ok_button.set_visible(True)
2024-01-20 15:32:45 +03:00
self._picon_box.set_sensitive(False)
2021-01-31 16:27:35 +03:00
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
2024-01-21 23:03:56 +03:00
class ExportM3uDialog(BaseDialog):
def __init__(self, app, bouquets):
super().__init__(app.app_window, "Export to m3u",
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, translate("Save"), Gtk.ResponseType.OK))
2024-01-21 23:03:56 +03:00
self._app = app
self._bouquets = bouquets
self._url = None
self._default_port = "8001"
2024-01-21 23:03:56 +03:00
builder = get_builder(f"{UI_RESOURCES_PATH}m3u.glade", use_str=True, objects=("export_m3u_box",))
self._main_grid = builder.get_object("export_m3u_grid")
2024-01-21 23:03:56 +03:00
self._port_entry = builder.get_object("export_port_entry")
self._port_auto_button = builder.get_object("export_auto_button")
2024-01-21 23:03:56 +03:00
self._all_type_button = builder.get_object("export_all_button")
self._iptv_type_button = builder.get_object("export_iptv_button")
2024-01-21 23:03:56 +03:00
self._grp_bq_button = builder.get_object("export_grp_bq_button")
self._grp_marker_button = builder.get_object("export_grp_markers_button")
self._bq_count_label = builder.get_object("export_bq_count_label")
self._services_count_label = builder.get_object("export_services_count_label")
self.get_content_area().pack_start(builder.get_object("export_m3u_box"), False, False, 0)
is_enigma = self._app.is_enigma
self._port_auto_button.set_active(True) if is_enigma else self._main_grid.remove_row(0)
self._grp_marker_button.set_visible(is_enigma)
self._all_type_button.set_active(True) if is_enigma else self._iptv_type_button.set_active(True)
self._all_type_button.set_sensitive(is_enigma)
2024-01-21 23:03:56 +03:00
self.connect("response", self.on_response)
self.connect("realize", self.init)
2024-01-21 23:03:56 +03:00
def init(self, widget=None):
self._bq_count_label.set_text(str(len(self._bouquets)))
self._services_count_label.set_text(str(len(list(chain.from_iterable(self._bouquets.values())))))
if self._app.is_enigma:
self._port_entry.connect("changed", self.on_port_changed)
self._port_auto_button.connect("toggled", self.on_port_auto_toggled)
# Add style for the port entry.
style_provider = Gtk.CssProvider()
style_provider.load_from_path(_CSS_PATH)
context = self._port_entry.get_style_context()
context.add_provider_for_screen(Gdk.Screen.get_default(), style_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
def on_port_changed(self, entry):
entry.set_name(_DIGIT_ENTRY_NAME if _PATTERN.search(entry.get_text()) else "GtkEntry")
def on_port_auto_toggled(self, button):
if not button.get_active() and not self._port_entry.get_text():
self._port_entry.set_text(self._default_port)
def on_response(self, dialog, response):
if response != Gtk.ResponseType.OK:
self.destroy()
else:
if self._app.is_enigma:
if self._port_auto_button.get_active():
self.do_export_auto()
else:
if self._port_entry.get_name() == _DIGIT_ENTRY_NAME:
self._app.show_error_message("Error. Verify the data!")
else:
st = self._app.app_settings
self._url = f"http{'s' if st.http_use_ssl else ''}://{st.host}:{self._port_entry.get_text()}/"
self.do_export()
else:
self.do_export()
return True
def do_export_auto(self, button=None):
""" Retrieves streaming port from Receiver via HTTP API and starts export.
Since the streaming port can be changed by the user,
we're getting base link to the stream -> http(s)://IP:PORT/
"""
from app.connections import HttpAPI
2024-01-21 23:03:56 +03:00
sent = self._app.send_http_request(HttpAPI.Request.STREAM, "", self.start_export)
self._port_auto_button.set_active(sent)
self._port_auto_button.set_sensitive(sent)
def start_export(self, data):
self._port_auto_button.set_active("error_code" not in data)
url = self._app.get_url_from_m3u(data)
url = urlparse(url)
if all((url.scheme, url.port)):
self._url = url.geturl()
self._port_entry.set_text(str(url.port))
self.do_export()
@run_idle
def do_export(self):
self.destroy()
services = self._app.current_services
def get_service(fav_id, num=0):
srv = services.get(fav_id, None)
if srv:
s_type = BqServiceType(srv.service_type)
if s_type is BqServiceType.DEFAULT:
srv = services.get(fav_id, None)
s_data = srv.picon_id.rstrip(".png").replace("_", ":") if srv.picon_id else None
return BouquetService(srv.service, s_type, s_data, num)
return BouquetService(srv.service, s_type, fav_id, num)
return BouquetService("N/A", BqServiceType.MARKER, fav_id, num)
# Preparing bouquets data.
bouquets = {b[:b.rindex(":")]: [get_service(i) for i in s] for b, s in self._bouquets.items()}
bq_services = []
s_types = {BqServiceType.IPTV}
if self._all_type_button.get_active():
s_types.add(BqServiceType.DEFAULT)
if self._grp_bq_button.get_active():
for b, bs in bouquets.items():
bq_services.append(BouquetService(b, BqServiceType.MARKER, None, 0))
bq_services.extend(filter(lambda s: s.type in s_types, bs))
elif self._grp_marker_button.get_active():
bq_services = chain.from_iterable(bouquets.values())
else:
bq_services = filter(lambda s: s.type in s_types, chain.from_iterable(bouquets.values()))
2024-01-21 23:03:56 +03:00
file_name = f"{'_'.join(list(bouquets)[:10])}__{date.today().strftime('%Y_%m_%d')}"
self._app.save_bouquet_to_m3u(bq_services, self._url, file_name)
2024-01-21 23:03:56 +03:00
2019-06-24 00:36:54 +03:00
class YtListImportDialog:
2022-02-21 12:22:44 +03:00
def __init__(self, app):
2019-06-24 00:36:54 +03:00
handlers = {"on_import": self.on_import,
"on_receive": self.on_receive,
"on_yt_url_entry_changed": self.on_url_entry_changed,
"on_yt_info_bar_close": self.on_info_bar_close,
2019-06-28 00:02:34 +03:00
"on_popup_menu": on_popup_menu,
2019-06-24 00:36:54 +03:00
"on_selected_toggled": self.on_selected_toggled,
2019-06-28 00:02:34 +03:00
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
"on_key_press": self.on_key_press,
2019-06-24 00:36:54 +03:00
"on_close": self.on_close}
2022-02-21 12:22:44 +03:00
self.appender = app.append_imported_services
self._settings = app.app_settings
self._s_type = self._settings.setting_type
2020-06-10 11:10:41 +03:00
self._download_task = False
self._yt_list_id = None
self._yt_list_title = None
2020-06-13 20:57:37 +03:00
self._yt = None
2020-06-10 11:10:41 +03:00
2021-04-28 14:12:59 +03:00
builder = get_builder(_UI_PATH, handlers, use_str=True,
objects=("yt_import_dialog_window", "yt_liststore", "yt_quality_liststore",
2021-08-23 23:52:51 +03:00
"yt_popup_menu", "remove_selection_image", "yt_receive_image",
"yt_import_image"))
2019-06-24 00:36:54 +03:00
self._dialog = builder.get_object("yt_import_dialog_window")
2022-02-21 12:22:44 +03:00
self._dialog.set_transient_for(app.app_window)
2019-06-24 00:36:54 +03:00
self._list_view_scrolled_window = builder.get_object("yt_list_view_scrolled_window")
self._model = builder.get_object("yt_liststore")
self._progress_bar = builder.get_object("yt_progress_bar")
self._info_bar_box = builder.get_object("yt_info_bar_box")
self._message_label = builder.get_object("yt_info_bar_message_label")
self._info_bar = builder.get_object("yt_info_bar")
2019-06-30 22:13:26 +03:00
self._yt_count_label = builder.get_object("yt_count_label")
2019-06-24 00:36:54 +03:00
self._url_entry = builder.get_object("yt_url_entry")
self._receive_button = builder.get_object("yt_receive_button")
self._import_button = builder.get_object("yt_import_button")
self._quality_box = builder.get_object("yt_quality_combobox")
self._quality_model = builder.get_object("yt_quality_liststore")
2023-03-30 00:13:25 +03:00
self._extract_switch = builder.get_object("yt_extract_links_switch")
2023-03-30 00:13:25 +03:00
self._url_prefix_combobox = builder.get_object("yt_url_prefix_combobox")
[self._url_prefix_combobox.append(v, k) for k, v in _URL_PREFIXES.items()]
self._url_prefix_combobox.set_active(0)
2021-10-16 14:37:21 +03:00
builder.get_object("yt_extract_links_box").set_visible(self._s_type is SettingsType.ENIGMA_2)
builder.get_object("yt_url_prefix_box").set_visible(self._s_type is SettingsType.ENIGMA_2)
2023-02-18 11:30:06 +03:00
if self._settings.use_header_bar:
2023-05-13 13:31:42 +03:00
header_bar = HeaderBar(title="YouTube", subtitle=translate("Playlist import"))
2023-01-30 17:36:22 +03:00
self._dialog.set_titlebar(header_bar)
actions_box = builder.get_object("yt_actions_box")
import_box = builder.get_object("yt_import_box")
actions_box.remove(import_box)
header_bar.pack_end(import_box)
actions_box.remove(self._receive_button)
header_bar.pack_start(self._receive_button)
actions_box.set_visible(False)
2021-10-16 14:37:21 +03:00
window_size = self._settings.get("yt_import_dialog_size")
if window_size:
self._dialog.resize(*window_size)
# Style.
2021-10-16 14:37:21 +03:00
style_provider = Gtk.CssProvider()
style_provider.load_from_path(_CSS_PATH)
2021-10-16 14:37:21 +03:00
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
2019-06-24 00:36:54 +03:00
Gtk.STYLE_PROVIDER_PRIORITY_USER)
def show(self):
self._dialog.show()
def on_import(self, item):
2019-06-26 15:57:22 +03:00
self.on_info_bar_close()
2019-06-24 00:36:54 +03:00
self.update_active_elements(False)
if self._extract_switch.get_active():
self.extract_direct_links()
else:
prefix = self._url_prefix_combobox.get_active_id()
selected = filter(lambda r: r[2], self._model)
2023-04-02 12:42:59 +03:00
prefix = quote(prefix) if prefix else ''
links = [(f"{prefix}https{quote(':')}//www.youtube.com/watch?v={r[1]}", r[0]) for r in selected]
self.append_services(links)
self.update_active_elements(True)
@run_task
def extract_direct_links(self):
2019-06-24 00:36:54 +03:00
self._download_task = True
try:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
done_links = {}
rows = list(filter(lambda r: r[2], self._model))
2020-06-13 20:57:37 +03:00
if not self._yt:
self._yt = YouTube.get_instance(self._settings)
futures = {executor.submit(self._yt.get_yt_link, r[1], YouTube.VIDEO_LINK.format(r[1]),
True): r for r in rows}
2019-06-24 00:36:54 +03:00
size = len(futures)
counter = 0
for future in concurrent.futures.as_completed(futures):
if not self._download_task:
executor.shutdown()
return
done_links[futures[future]] = future.result()
2019-06-24 00:36:54 +03:00
counter += 1
self.update_progress_bar(counter / size)
2020-06-13 20:57:37 +03:00
except YouTubeException as e:
2020-06-10 11:10:41 +03:00
self.show_info_message(str(e), Gtk.MessageType.ERROR)
2019-06-24 00:36:54 +03:00
except Exception as e:
2019-06-27 22:10:44 +03:00
self.show_info_message(str(e), Gtk.MessageType.ERROR)
2019-06-24 00:36:54 +03:00
else:
2019-06-27 22:10:44 +03:00
if self._download_task:
self.append_services([done_links[r] for r in rows])
2019-06-24 00:36:54 +03:00
finally:
self._download_task = False
self.update_active_elements(True)
def on_receive(self, item):
self.update_active_elements(False)
self._model.clear()
2019-06-30 22:13:26 +03:00
self._yt_count_label.set_text("0")
2019-06-24 00:36:54 +03:00
self.on_info_bar_close()
self.update_refs_list()
@run_task
def update_refs_list(self):
if self._yt_list_id:
try:
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())
2019-06-24 00:36:54 +03:00
except Exception as e:
2019-06-27 22:10:44 +03:00
self.show_info_message(str(e), Gtk.MessageType.ERROR)
2019-06-24 00:36:54 +03:00
return
else:
gen = self.update_links(links)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
finally:
self.update_active_elements(True)
def update_links(self, links):
2020-06-10 11:10:41 +03:00
for link in links:
yield self._model.append((link[0], link[1], True, None))
2019-06-24 00:36:54 +03:00
size = len(self._model)
self._yt_count_label.set_text(str(size))
self._import_button.set_visible(size)
2019-06-30 22:13:26 +03:00
yield True
2019-06-24 00:36:54 +03:00
@run_idle
def append_services(self, links):
2019-06-27 22:10:44 +03:00
aggr = [None] * 9
2019-06-26 15:57:22 +03:00
srvs = []
if self._yt_list_title and self._s_type is SettingsType.ENIGMA_2:
2019-06-26 15:57:22 +03:00
title = self._yt_list_title
2019-09-04 10:39:46 +03:00
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)
2019-06-27 22:10:44 +03:00
srvs.append(mk)
extract = self._extract_switch.get_active()
act = self._quality_model.get_value(self._quality_box.get_active_iter(), 0) if extract else None
for link in links:
2020-06-10 11:10:41 +03:00
lnk, title = link or (None, None)
2019-08-18 17:02:32 +03:00
if not lnk:
continue
if extract:
ln = lnk.get(act) if act in lnk else lnk[sorted(lnk, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]
else:
ln = lnk
fav_id = get_fav_id(ln, title, self._s_type, force_quote=extract)
srv = Service(None, None, IPTV_ICON, title, *aggr[0:3], BqServiceType.IPTV.name, *aggr, None, fav_id, None)
2019-06-28 00:02:34 +03:00
srvs.append(srv)
2019-06-26 15:57:22 +03:00
self.appender(srvs)
2024-01-20 19:04:20 +03:00
self.show_info_message("Done!", Gtk.MessageType.INFO)
2019-06-24 00:36:54 +03:00
@run_idle
def update_active_elements(self, sensitive):
self._url_entry.set_sensitive(sensitive)
self._receive_button.set_sensitive(sensitive)
def on_url_entry_changed(self, entry):
url_str = entry.get_text()
yt_id = YouTube.get_yt_list_id(url_str)
entry.set_name("GtkEntry" if yt_id else _DIGIT_ENTRY_NAME)
self._receive_button.set_sensitive(bool(yt_id))
self._import_button.set_sensitive(bool(yt_id))
2019-06-24 00:36:54 +03:00
self._yt_list_id = yt_id
if yt_id:
entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
else:
entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
@run_idle
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_idle
def update_progress_bar(self, value):
2019-06-30 22:13:26 +03:00
self._progress_bar.set_visible(value < 1)
2019-06-24 00:36:54 +03:00
self._progress_bar.set_fraction(value)
@run_idle
def show_info_message(self, text, message_type):
2024-01-21 23:03:56 +03:00
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
2019-06-24 00:36:54 +03:00
def on_selected_toggled(self, toggle, path):
self._model.set_value(self._model.get_iter(path), 2, not toggle.get_active())
2019-06-28 00:02:34 +03:00
def on_select_all(self, view):
self.update_selection(view, True)
def on_unselect_all(self, view):
self.update_selection(view, False)
def update_selection(self, view, select):
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 2, select))
def on_key_press(self, view, event):
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
if key is KeyboardKey.SPACE:
path, column = view.get_cursor()
itr = self._model.get_iter(path)
selected = self._model.get_value(itr, 2)
self._model.set_value(itr, 2, not selected)
2019-06-24 00:36:54 +03:00
def on_close(self, window, event):
if self._download_task and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
2019-06-27 22:10:44 +03:00
return True
2021-10-16 14:37:21 +03:00
2019-06-24 00:36:54 +03:00
self._download_task = False
2021-10-16 14:37:21 +03:00
self._settings.add("yt_import_dialog_size", self._dialog.get_size())
2019-06-24 00:36:54 +03:00
2018-03-11 21:52:10 +03:00
if __name__ == "__main__":
pass