# TG-UserBot - A modular Telegram UserBot script for Python. # Copyright (C) 2019 Kandarp <https://github.com/kandnub> # # TG-UserBot is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # TG-UserBot is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with TG-UserBot. If not, see <https://www.gnu.org/licenses/>. import concurrent import functools import os import pathlib import re import time import youtube_dl from .. import LOGGER downloads = {} audio = re.compile(r'\[ffmpeg\] Destination\: (.+)') video = re.compile( r'\[ffmpeg\] Converting video from \w+ to \w+, Destination: (.+)' ) merger = re.compile(r'\[ffmpeg\] Merging formats into "(.+)"') class YTdlLogger(object): """Logger used for YoutubeDL which logs to UserBot logger.""" def debug(self, msg: str) -> None: """Logs debug messages with youtube-dl tag to UserBot logger.""" LOGGER.debug("youtube-dl: " + msg) f = None if "[ffmpeg]" in msg: if audio.search(msg): f = audio.match(msg).group(1) if video.search(msg): f = video.match(msg).group(1) if merger.search(msg): f = merger.match(msg).group(1) if f: downloads.update({f.split('.')[0]: f}) def warning(self, msg: str) -> None: """Logs warning messages with youtube-dl tag to UserBot logger.""" LOGGER.warning("youtube-dl: " + msg) def error(self, msg: str) -> None: """Logs error messages with youtube-dl tag to UserBot logger.""" LOGGER.error("youtube-dl: " + msg) def critical(self, msg: str) -> None: """Logs critical messages with youtube-dl tag to UserBot logger.""" LOGGER.critical("youtube-dl: " + msg) class ProgressHook(): """Custom hook with the event stored for YTDL.""" def __init__(self, event, update=5): self.event = event self.downloaded = 0 self.tasks = [] self.update = update self.last_edit = None def callback(self, task): """Cancel pending tasks else skip them if completed.""" if task.cancelled(): return else: new = task.result().date if new > self.last_edit: self.last_edit = new def edit(self, *args, **kwargs): """Create a Task of the progress edit.""" task = self.event.client.loop.create_task( self.event.answer(*args, **kwargs) ) # task.add_done_callback(self.callback) self.tasks.append(task) return task def hook(self, d: dict) -> None: """ YoutubeDL's hook which logs progress and errors to UserBot logger. """ if d['status'] == 'downloading': filen = d.get('filename', 'Unknown filename') prcnt = d.get('_percent_str', None) ttlbyt = d.get('_total_bytes_str', None) spdstr = d.get('_speed_str', None) etastr = d.get('_eta_str', None) if not prcnt or not ttlbyt or not spdstr or not etastr: return finalStr = ( "Downloading {}: {} of {} at {} ETA: {}".format( filen, prcnt, ttlbyt, spdstr, etastr ) ) LOGGER.debug(finalStr) if float(prcnt[:-1]) - self.downloaded >= self.update: # Avoid spamming recents if self.last_edit and time.time() - self.last_edit < 10: return self.downloaded = float(prcnt[:-1]) filen = re.sub(r'YT_DL\\(.+)_\d+\.', r'\1.', filen) self.edit( f"`Downloading {filen} at {spdstr}.`\n" f"__Progress: {prcnt} of {ttlbyt}__\n" f"__ETA: {etastr}__" ) self.last_edit = time.time() elif d['status'] == 'finished': filen = d.get('filename', 'Unknown filename') filen1 = re.sub(r'YT_DL\\(.+)_\d+\.', r'\1.', filen) ttlbyt = d.get('_total_bytes_str', None) elpstr = d.get('_elapsed_str', None) downloads.update({filen.split('.')[0]: filen}) if not ttlbyt or not elpstr: return finalStr = f"Downloaded {filen}: 100% of {ttlbyt} in {elpstr}" LOGGER.warning(finalStr) self.event.client.loop.create_task( self.event.answer( f"`Successfully downloaded {filen1} in {elpstr}!`" ) ) for task in self.tasks: if not task.done(): task.cancel() self.tasks.clear() elif d['status'] == 'error': finalStr = "Error: " + str(d) LOGGER.error(finalStr) async def list_formats(info_dict: dict) -> str: """YoutubeDL's list_formats method but without format notes. Args: info_dict (``dict``): Dictionary which is returned by YoutubeDL's extract_info method. Returns: ``str``: All available formats in order as a string instead of stdout. """ formats = info_dict.get('formats', [info_dict]) table = [ [f['format_id'], f['ext'], youtube_dl.YoutubeDL.format_resolution(f)] for f in formats if f.get('preference') is None or f['preference'] >= -1000] if len(formats) > 1: table[-1][-1] += (' ' if table[-1][-1] else '') + '(best)' header_line = ['format code', 'extension', 'resolution'] fmtStr = ( '`Available formats for %s:`\n`%s`' % (info_dict['title'], youtube_dl.render_table(header_line, table)) ) return fmtStr async def extract_info( loop, executor: concurrent.futures.Executor, ydl_opts: dict, url: str, download: bool = False ) -> str: """Runs YoutubeDL's extract_info method without blocking the event loop. Args: executor (:obj:`concurrent.futures.Executor <concurrent.futures>`): Either ``ThreadPoolExecutor`` or ``ProcessPoolExecutor``. params (``dict``): Parameters/Keyword arguments to use for YoutubeDL. url (``str``): The url which you want to use for extracting info. download (``bool``, optional): If you want to download the video. Defaults to False. Returns: ``str``: Successfull string or info_dict on success or an exception's string if any occur. """ ydl_opts['outtmpl'] = ydl_opts['outtmpl'].format(time=time.time_ns()) ytdl = youtube_dl.YoutubeDL(ydl_opts) def downloader(url, download): eStr = None try: info_dict = ytdl.extract_info(url, download=download) except youtube_dl.utils.DownloadError as DE: eStr = f"`{DE}`" except youtube_dl.utils.ContentTooShortError: eStr = "`There download content was too short.`" except youtube_dl.utils.GeoRestrictedError: eStr = ( "`Video is not available from your geographic location due " "to geographic restrictions imposed by a website.`" ) except youtube_dl.utils.MaxDownloadsReached: eStr = "`Max-downloads limit has been reached.`" except youtube_dl.utils.PostProcessingError: eStr = "`There was an error during post processing.`" except youtube_dl.utils.UnavailableVideoError: eStr = "`Video is not available in the requested format.`" except youtube_dl.utils.XAttrMetadataError as XAME: eStr = f"`{XAME.code}: {XAME.msg}\n{XAME.reason}`" except youtube_dl.utils.ExtractorError: eStr = "`There was an error during info extraction.`" except Exception as e: return e if eStr: return eStr if download: filen = ytdl.prepare_filename(info_dict) opath = downloads.pop(filen.rsplit('.', maxsplit=1)[0], filen) downloaded = pathlib.Path(opath) if not downloaded.exists(): pattern = f"*{info_dict['title']}*" for f in pathlib.Path(downloaded.parent).glob(pattern): if f.suffix != ".jpg": opath = f"YT_DL/{f.name}{f.suffix}" break npath = re.sub(r'_\d+(\.\w+)$', r'\1', opath) thumb = pathlib.Path(re.sub(r'\.\w+$', r'.jpg', opath)) old_f = pathlib.Path(npath) new_f = pathlib.Path(opath) if old_f.exists(): if old_f.samefile(new_f): os.remove(str(new_f.absolute())) else: newname = str(old_f.stem) + '_OLD' old_f.replace( old_f.with_name(newname).with_suffix(old_f.suffix) ) path = new_f.parent.parent / npath new_f.rename(new_f.parent.parent / npath) thumb = str(thumb.absolute()) if thumb.exists() else None return path.absolute(), thumb, info_dict else: return info_dict # Future blocks the running event loop # fut = executor.submit(downloader, url, download) # result = fut.result() result = None try: result = await loop.run_in_executor( concurrent.futures.ThreadPoolExecutor(), functools.partial(downloader, url, download) ) except Exception as e: result = f"```{type(e)}: {e}```" finally: return result