From 8fee65cabb2c3ef5cd0b1142ca4755d9bdffb5d5 Mon Sep 17 00:00:00 2001 From: DYefremov Date: Mon, 24 Jun 2019 00:36:54 +0300 Subject: [PATCH] yt list import dialog skeleton --- app/tools/yt.py | 10 +- app/ui/iptv.py | 155 ++++++++- app/ui/main_app_window.py | 11 +- app/ui/main_window.glade | 689 +++++++++++++++++++------------------- app/ui/yt_dialog.glade | 273 +++++++++++++++ 5 files changed, 797 insertions(+), 341 deletions(-) create mode 100644 app/ui/yt_dialog.glade diff --git a/app/tools/yt.py b/app/tools/yt.py index 0c5895c0..60acfa79 100644 --- a/app/tools/yt.py +++ b/app/tools/yt.py @@ -4,7 +4,8 @@ import urllib from html.parser import HTMLParser from urllib.request import Request -_YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=|\/)([\w-]{11})&?(list=)?([\w-]{34})?.*") +_YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=)([\w-]{11}).*") +_YT_LIST_PATTERN = re.compile(r"https://www.youtube.com/.+?(?:list=)([\w-]{34})?.*") _YT_VIDEO_PATTERN = re.compile(r"https://r\d+---sn-[\w]{10}-[\w]{3,5}.googlevideo.com/videoplayback?.*") _HEADERS = {"User-Agent": "Mozilla/5.0"} @@ -23,6 +24,13 @@ class YouTube: if yt: return yt.group(1) + @staticmethod + def get_yt_list_id(url): + """ Returns playlist id or None """ + yt = re.search(_YT_LIST_PATTERN, url) + if yt: + return yt.group(1) + @staticmethod def get_yt_link(video_id): """ Getting link to YouTube video by id. diff --git a/app/ui/iptv.py b/app/ui/iptv.py index 7c3130e1..92898c00 100644 --- a/app/ui/iptv.py +++ b/app/ui/iptv.py @@ -1,3 +1,4 @@ +import concurrent.futures import glob import os import re @@ -11,9 +12,9 @@ from gi.repository import GLib from app.commons import run_idle, run_task from app.eparser.ecommons import BqServiceType, Service -from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT +from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id from app.properties import Profile -from app.tools.yt import YouTube +from app.tools.yt import YouTube, PlayListParser from .dialogs import Action, show_dialog, DialogType, get_dialogs_string, get_message from .main_helper import get_base_model, get_iptv_url from .uicommons import Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON, Column, IS_GNOME_SESSION @@ -543,5 +544,155 @@ class IptvListConfigurationDialog: self.update_reference() +class YtListImportDialog: + def __init__(self, transient, bouquet, fav_view, profile): + 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, + "on_selected_toggled": self.on_selected_toggled, + "on_close": self.on_close} + + builder = Gtk.Builder() + builder.set_translation_domain(TEXT_DOMAIN) + builder.add_objects_from_string( + get_dialogs_string(UI_RESOURCES_PATH + "yt_dialog.glade").format(use_header=IS_GNOME_SESSION), + ("yt_import_dialog_window", "yt_liststore")) + builder.connect_signals(handlers) + + self._dialog = builder.get_object("yt_import_dialog_window") + self._dialog.set_transient_for(transient) + 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") + 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") + # style + 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._bouquet = bouquet + self._fav_path, c = fav_view.get_cursor() + self._fav_model = fav_view.get_model() + self._profile = profile + self._download_task = False + self._yt_list_id = None + self._yt_list_title = None + + def show(self): + self._dialog.show() + + @run_task + def on_import(self, item): + self.update_active_elements(False) + self._download_task = True + + try: + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: + done_links = [] + futures = {executor.submit(YouTube.get_yt_link, r[1]): r for r in self._model if r[2]} + size = len(futures) + counter = 0 + + for future in concurrent.futures.as_completed(futures): + if not self._download_task: + executor.shutdown() + return + done_links.append(future.result()) + counter += 1 + self.update_progress_bar(counter / size) + except Exception as e: + print(e) + else: + self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO) + self.append_services(done_links) + finally: + self._download_task = False + self.update_active_elements(True) + + def on_receive(self, item): + self.update_active_elements(False) + self._model.clear() + self.on_info_bar_close() + self.update_refs_list() + + @run_task + def update_refs_list(self): + if self._yt_list_id: + try: + self._yt_list_title, links = PlayListParser.get_yt_playlist(self._yt_list_id) + except Exception as e: + print(e) + 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): + for l in links: + yield self._model.append((l[0], l[1], True, None)) + + @run_idle + def append_services(self, links): + aggr = [None] * 10 + print("List title:", self._yt_list_title) + for l in links: + fav_id = get_fav_id(*l, self._profile) + srv = Service(None, None, IPTV_ICON, l[1], *aggr[0:3], BqServiceType.IPTV.name, *aggr, fav_id, None) + print(srv) + + @run_idle + def update_active_elements(self, sensitive): + self._list_view_scrolled_window.set_visible(sensitive or not sensitive) + self._url_entry.set_sensitive(sensitive) + self._receive_button.set_sensitive(sensitive) + self._import_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._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): + self._info_bar_box.set_visible(value < 1) + self._progress_bar.set_fraction(value) + + @run_idle + def show_info_message(self, text, message_type): + self._info_bar.set_visible(True) + self._info_bar.set_message_type(message_type) + self._message_label.set_text(text) + + def on_selected_toggled(self, toggle, path): + self._model.set_value(self._model.get_iter(path), 2, not toggle.get_active()) + + @run_idle + def on_close(self, window, event): + if self._download_task and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL: + return + self._download_task = False + self._dialog.destroy() + + if __name__ == "__main__": pass diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index b099ea93..00a5beb0 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -22,7 +22,7 @@ from app.ui.epg_dialog import EpgDialog from .backup import BackupDialog, backup_data, clear_data_path from .imports import ImportDialog, import_bouquet from .download_dialog import DownloadDialog -from .iptv import IptvDialog, SearchUnavailableDialog, IptvListConfigurationDialog +from .iptv import IptvDialog, SearchUnavailableDialog, IptvListConfigurationDialog, YtListImportDialog from .search import SearchProvider from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON, IPTV_ICON, MOVE_KEYS, KeyboardKey, Column, \ EXTRA_COLOR, NEW_COLOR, FavClickMode @@ -110,6 +110,7 @@ class Application(Gtk.Application): "on_hide": self.on_hide, "on_locked": self.on_locked, "on_model_changed": self.on_model_changed, + "on_import_yt_list": self.on_import_yt_list, "on_import_m3u": self.on_import_m3u, "on_export_to_m3u": self.on_export_to_m3u, "on_import_bouquet": self.on_import_bouquet, @@ -1373,6 +1374,14 @@ class Application(Gtk.Application): # ***************** Import ********************# + def on_import_yt_list(self, item): + """ Import playlist from YouTube """ + if not self._bq_selected: + return + + bq = self._bouquets.get(self._bq_selected, []) + YtListImportDialog(self._main_window, bq, self._fav_view, Profile(self._profile)).show() + def on_import_m3u(self, item): """ Imports iptv from m3u files. """ response = get_chooser_dialog(self._main_window, self._options.get(self._profile), "*.m3u", "m3u files") diff --git a/app/ui/main_window.glade b/app/ui/main_window.glade index c4ed8223..cbe3b705 100644 --- a/app/ui/main_window.glade +++ b/app/ui/main_window.glade @@ -582,343 +582,6 @@ Author: Dmitriy Yefremov False gtk-remove - - True - False - - - gtk-cut - True - False - False - True - True - - - - - - - gtk-copy - True - False - False - True - True - - - - - - - gtk-paste - True - False - False - True - True - - - - - - - True - False - - - - - gtk-edit - True - False - True - True - - - - - - - Extra: - True - False - extra_edit_image - False - True - - - True - False - - - Rename for this bouquet - True - False - edit_image_1 - False - - - - - - Set default name - True - False - edit_image_2 - False - - - - - - - - - - True - False - - - - - Locate in services - True - False - False - find_image - False - - - - - - True - False - - - - - Insert marker - True - False - False - True - insert_text_image - False - - - - - - True - False - - - - - IPTV - True - False - False - networktransmit_receive_image - False - - - True - False - - - gtk-media-play - True - False - True - True - - - - - - True - False - - - - - Add IPTV or stream service - True - False - network_transmit_receive_image - False - - - - - - Import m3u - True - False - True - downloads_image - False - - - - - - Export to m3u - True - False - export_to_m3u_image - False - - - - - - True - False - - - - - EPG configuration - True - False - epg_configuration_image - False - - - - - - True - False - - - - - List configuration - True - False - fav_iptv_list_config_image - False - - - - - - True - False - - - - - Remove all unavailable - True - False - remova_image - False - - - - - - - - - - True - False - - - - - Picon - True - False - False - insert_image - False - - - True - False - - - Assign - True - False - insert_link_image_2 - False - - - - - - Remove - True - False - clear_image_2 - False - - - - - - True - False - - - - - Copy reference - True - False - copy_image_2 - False - - - - - - True - False - - - - - Remove all unused - True - False - remove_all_image - False - - - - - - - - - - True - False - - - - - gtk-remove - True - False - False - True - True - - - - - True False @@ -3114,4 +2777,356 @@ Author: Dmitriy Yefremov + + True + False + gtk-connect + + + True + False + + + gtk-cut + True + False + False + True + True + + + + + + + gtk-copy + True + False + False + True + True + + + + + + + gtk-paste + True + False + False + True + True + + + + + + + True + False + + + + + gtk-edit + True + False + True + True + + + + + + + Extra: + True + False + extra_edit_image + False + True + + + True + False + + + Rename for this bouquet + True + False + edit_image_1 + False + + + + + + Set default name + True + False + edit_image_2 + False + + + + + + + + + + True + False + + + + + Locate in services + True + False + False + find_image + False + + + + + + True + False + + + + + Insert marker + True + False + False + True + insert_text_image + False + + + + + + True + False + + + + + IPTV + True + False + False + networktransmit_receive_image + False + + + True + False + + + gtk-media-play + True + False + True + True + + + + + + True + False + + + + + Add IPTV or stream service + True + False + network_transmit_receive_image + False + + + + + + Import YouTube playlist + True + False + yt_image + False + + + + + + Import m3u + True + False + True + downloads_image + False + + + + + + Export to m3u + True + False + export_to_m3u_image + False + + + + + + True + False + + + + + EPG configuration + True + False + epg_configuration_image + False + + + + + + True + False + + + + + List configuration + True + False + fav_iptv_list_config_image + False + + + + + + True + False + + + + + Remove all unavailable + True + False + remova_image + False + + + + + + + + + + True + False + + + + + Picon + True + False + False + insert_image + False + + + True + False + + + Assign + True + False + insert_link_image_2 + False + + + + + + Remove + True + False + clear_image_2 + False + + + + + + True + False + + + + + Copy reference + True + False + copy_image_2 + False + + + + + + True + False + + + + + Remove all unused + True + False + remove_all_image + False + + + + + + + + + + True + False + + + + + gtk-remove + True + False + False + True + True + + + + + diff --git a/app/ui/yt_dialog.glade b/app/ui/yt_dialog.glade new file mode 100644 index 00000000..9e2b564e --- /dev/null +++ b/app/ui/yt_dialog.glade @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + 480 + False + True + center-on-parent + True + True + True + center + + + + True + False + 2 + True + + + True + False + True + True + Receive + + + + True + False + gtk-goto-bottom + + + + + 1 + + + + + True + True + 2 + 2 + True + gtk-edit + YouTube playlist URL: + + + + + + True + True + True + Import + + + + True + False + insert-link + + + + + end + 1 + + + + + + + True + False + vertical + 2 + + + True + in + 120 + 480 + + + True + True + yt_liststore + 0 + horizontal + 3 + + + + + + True + 50 + Title + True + True + 0.5 + True + 0 + + + 5 + + + 0 + + + + + + + False + ID + + + + 1 + + + + + + + 50 + 100 + Selected + True + 0.5 + True + 2 + + + 50 + + + + 2 + + + + + + + False + Tooltip + + + + 3 + + + + + + + + + True + True + 2 + + + + + 24 + False + 2 + + + + + + 10 + True + False + center + center + 0.01 + + + True + True + 1 + + + + + + + + False + True + 3 + + + + + False + True + + + + False + 6 + end + + + + + + False + False + 0 + + + + + False + 16 + + + True + False + start + info + + + True + True + 0 + + + + + False + False + 0 + + + + + + + + False + True + 4 + + + + + +