diff --git a/app/tools/yt.py b/app/tools/yt.py index 60d767a7..b4500db5 100644 --- a/app/tools/yt.py +++ b/app/tools/yt.py @@ -14,7 +14,7 @@ from urllib.request import Request, urlopen, urlretrieve from app.commons import log _YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=)([\w-]{11}).*") -_YT_LIST_PATTERN = re.compile(r"https://www.youtube.com/.+?(?:list=)([\w-]{23,})?.*") +_YT_LIST_PATTERN = re.compile(r"https://www.youtube.com/.+?(?:list=)([\w-]{18,})?.*") _YT_VIDEO_PATTERN = re.compile(r"https://r\d+---sn-[\w]{10}-[\w]{3,5}.googlevideo.com/videoplayback?.*") _HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/69.0", "DNT": "1", @@ -24,11 +24,32 @@ Quality = {137: "1080p", 136: "720p", 135: "480p", 134: "360p", 133: "240p", 160: "144p", 0: "0p", 18: "360p", 22: "720p"} +class YouTubeException(Exception): + pass + + class YouTube: """ Helper class for working with YouTube service. """ + _YT_INSTANCE = None _VIDEO_INFO_LINK = "https://youtube.com/get_video_info?video_id={}&hl=en" + VIDEO_LINK = "https://www.youtube.com/watch?v={}" + + def __init__(self, settings, callback): + self._settings = settings + self._yt_dl = None + self._callback = callback + + if self._settings.enable_yt_dl: + self._yt_dl = YouTubeDL.get_instance(self._settings, callback=self._callback) + + @classmethod + def get_instance(cls, settings, callback=log): + if not cls._YT_INSTANCE: + cls._YT_INSTANCE = YouTube(settings, callback) + return cls._YT_INSTANCE + @staticmethod def is_yt_video_link(url): return re.match(_YT_VIDEO_PATTERN, url) @@ -47,12 +68,24 @@ class YouTube: if yt: return yt.group(1) - @staticmethod - def get_yt_link(video_id): - """ Getting link to YouTube video by id. + def get_yt_link(self, video_id, url=None, skip_errors=False): + """ Getting link to YouTube video by id or URL. - returns tuple from the video links dict and title + Returns tuple from the video links dict and title. """ + if self._settings.enable_yt_dl and url: + if not self._yt_dl: + self._yt_dl = YouTubeDL.get_instance(self._settings, self._callback) + return self._yt_dl.get_yt_link(url, skip_errors) + + return self.get_yt_link_by_id(video_id) + + @staticmethod + def get_yt_link_by_id(video_id): + """ Getting link to YouTube video by id. + + Returns tuple from the video links dict and title. + """ req = Request(YouTube._VIDEO_INFO_LINK.format(video_id), headers=_HEADERS) with urlopen(req, timeout=2) as resp: @@ -169,12 +202,8 @@ class YouTubeDL: _LATEST_RELEASE_URL = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest" _OPTIONS = {"noplaylist": True, # Single video instead of a playlist [ignoring playlist in URL]. "quiet": True, # Do not print messages to stdout. - "simulate": True} # Do not download the video files. - - VIDEO_LINK = "https://www.youtube.com/watch?v={}" - - class YouTubeDLException(Exception): - pass + "simulate": True, # Do not download the video files. + "cookiefile": "cookies.txt"} # File name where cookies should be read from and dumped to. def __init__(self, settings, callback): self._path = settings.default_data_path + "tools/" @@ -185,6 +214,8 @@ class YouTubeDL: self._download_exception = None self._is_update_process = False + self.init() + @classmethod def get_instance(cls, settings, callback=print): if not cls._DL_INSTANCE: @@ -205,7 +236,7 @@ class YouTubeDL: import youtube_dl except ModuleNotFoundError as e: log("YouTubeDLHelper error: {}".format(str(e))) - raise self.YouTubeDLException(e) + raise YouTubeException(e) else: if self._update: if hasattr(youtube_dl.version, "__version__"): @@ -221,7 +252,8 @@ class YouTubeDL: self._dl = youtube_dl.YoutubeDL(self._OPTIONS) log("youtube-dl initialized...") - def get_last_release_id(self): + @staticmethod + def get_last_release_id(): """ Getting last release id. """ url = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest" with urlopen(url, timeout=10) as resp: @@ -255,21 +287,18 @@ class YouTubeDL: arch.extract(info.filename) shutil.move(info.filename, "{}{}{}".format(self._path, sep, f)) shutil.rmtree(pref) - msg = "Getting the last youtube-dl release is done! Please restart." + msg = "Getting the last youtube-dl release is done!" log(msg) self._callback(msg, False) return True except URLError as e: log("YouTubeDLHelper error: {}".format(e)) - raise self.YouTubeDLException(e) + raise YouTubeException(e) finally: self._is_update_process = False def get_yt_link(self, url, skip_errors=False): """ Returns tuple from the video links [dict] and title. """ - if not self._dl: - self.init() - if self._is_update_process: self._callback("Update process. Please wait.", False) return {}, "" @@ -278,11 +307,11 @@ class YouTubeDL: info = self._dl.extract_info(url, download=False) except URLError as e: log(str(e)) - raise self.YouTubeDLException(e) + raise YouTubeException(e) except self._DownloadError as e: log(str(e)) if not skip_errors: - raise self.YouTubeDLException(e) + raise YouTubeException(e) else: fmts = info.get("formats", None) if fmts: diff --git a/app/ui/iptv.py b/app/ui/iptv.py index bfd21cec..3ade0079 100644 --- a/app/ui/iptv.py +++ b/app/ui/iptv.py @@ -11,7 +11,7 @@ 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, get_fav_id, MARKER_FORMAT from app.settings import SettingsType -from app.tools.yt import YouTube, PlayListParser, YouTubeDL +from app.tools.yt import PlayListParser, YouTubeException, YouTube from .dialogs import Action, show_dialog, DialogType, get_dialogs_string, get_message from .main_helper import get_base_model, get_iptv_url, on_popup_menu from .uicommons import (Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON, Column, IS_GNOME_SESSION, KeyboardKey, @@ -219,20 +219,19 @@ class IptvDialog: def set_yt_url(self, entry, video_id): try: - if self._settings.enable_yt_dl: - 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 = YouTubeDL.get_instance(self._settings, callback=callback) - yield True - links, title = self._yt_dl.get_yt_link(entry.get_text()) - else: - links, title = YouTube.get_yt_link(video_id) + 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: self.show_info_message(get_message("Getting link error:") + (str(e)), Gtk.MessageType.ERROR) return - except YouTubeDL.YouTubeDLException as e: + except YouTubeException as e: self.show_info_message((str(e)), Gtk.MessageType.ERROR) return else: @@ -581,7 +580,7 @@ class YtListImportDialog: self._yt_list_id = None self._yt_list_title = None self._settings = settings - self._yt_dl = None + self._yt = None builder = Gtk.Builder() builder.set_translation_domain(TEXT_DOMAIN) @@ -626,14 +625,11 @@ class YtListImportDialog: with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: done_links = {} rows = list(filter(lambda r: r[2], self._model)) - if self._settings.enable_yt_dl: - if not self._yt_dl: - self._yt_dl = YouTubeDL.get_instance(self._settings) - futures = {executor.submit(self._yt_dl.get_yt_link, - YouTubeDL.VIDEO_LINK.format(r[1]), - True): r for r in rows} - else: - futures = {executor.submit(YouTube.get_yt_link, r[1]): r for r in rows} + 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} size = len(futures) counter = 0 @@ -645,7 +641,7 @@ class YtListImportDialog: done_links[futures[future]] = future.result() counter += 1 self.update_progress_bar(counter / size) - except YouTubeDL.YouTubeDLException as e: + except YouTubeException as e: self.show_info_message(str(e), Gtk.MessageType.ERROR) except Exception as e: self.show_info_message(str(e), Gtk.MessageType.ERROR) diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index 2cb1a4cb..405ac78e 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -2084,7 +2084,7 @@ class Application(Gtk.Application): @run_idle def init_send_to(self, enable): if enable and not self._links_transmitter: - self._links_transmitter = LinksTransmitter(self._http_api, self._main_window) + self._links_transmitter = LinksTransmitter(self._http_api, self._main_window, self._settings) elif self._links_transmitter: self._links_transmitter.show(enable) diff --git a/app/ui/transmitter.py b/app/ui/transmitter.py index 6af5c165..f3a13ead 100644 --- a/app/ui/transmitter.py +++ b/app/ui/transmitter.py @@ -18,7 +18,7 @@ class LinksTransmitter: """ __STREAM_PREFIX = "4097:0:1:0:0:0:0:0:0:0:" - def __init__(self, http_api, app_window): + def __init__(self, http_api, app_window, settings): handlers = {"on_popup_menu": self.on_popup_menu, "on_status_icon_activate": self.on_status_icon_activate, "on_url_changed": self.on_url_changed, @@ -45,6 +45,7 @@ class LinksTransmitter: self._restore_menu_item = builder.get_object("restore_menu_item") self._status_active = None self._status_passive = None + self._yt = YouTube.get_instance(settings) try: gi.require_version("AppIndicator3", "0.1") @@ -113,7 +114,7 @@ class LinksTransmitter: if yt_id: self._url_entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32)) - links, title = YouTube.get_yt_link(yt_id) + links, title = self._yt.get_yt_link(yt_id, url) yield True if links: url = links[sorted(links, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]