diff --git a/hubot.py b/hubot.py index 04e8f83..5231b8b 100644 --- a/hubot.py +++ b/hubot.py @@ -94,6 +94,11 @@ async def _cb_new_message(event) -> None: tasks = [] # pass the event to all mods interested in it for mod_name in _mods: + # mod is not enabled for this user? + if mod_name not in sessions[name]['config']['accounts'][name]: + continue + if not sessions[name]['config']['accounts'][name][mod_name]: + continue # mod mod = _mods[mod_name] try: diff --git a/mod_video_downloader.py b/mod_video_downloader.py index 51eaa98..84edd53 100644 --- a/mod_video_downloader.py +++ b/mod_video_downloader.py @@ -12,6 +12,7 @@ import os from telethon import events from telethon.tl.types import PeerUser from telethon.tl.types import DocumentAttributeVideo +from telethon.tl.types import DocumentAttributeAudio import utils @@ -32,7 +33,7 @@ def _get_all_qualities_raw(url: str, proxy: bool, timeout: float = 20) -> dict | try: # prepare arguments args = [ - utils.which('youtube-dl'), + utils.which('yt-dlp'), '-J', url ] @@ -50,22 +51,44 @@ def _get_all_qualities_raw(url: str, proxy: bool, timeout: float = 20) -> dict | j = json.loads(txt) except: return cp.stderr.decode(encoding='ascii') + if not j: + return 'yt-dlp has failed to get the list of qualities' + # dict to return res = {} + title = j['title'] if 'title' in j else 'Без названия' + author = j['channel'] if 'channel' in j else 'Неизвестен' + thumbnail = j['thumbnail'] if 'thumbnail' in j else None # one format only - convert to many if 'formats' not in j: j['formats'] = [{ + 'format': j['format'], 'format_id': j['format_id'], - 'ext': j['ext'] + 'ext': j['ext'], + 'video_ext': j['video_ext'], + 'audio_ext': j['audio_ext'] }] + if 'filesize' in j: + j['formats'][0]['filesize'] = j['filesize'] # check all formats for f in j['formats']: obj = { + 'title': title, + 'author': author, 'url': url, 'format': f['format_id'], + 'thumbnail': thumbnail, 'ext': f['ext'], + 'format_name': f['format'], + 'is_audio': f['audio_ext'] != 'none', '_added': time.time() } + # filesize exists + try: + if 'filesize' in f: + obj['filesize'] = int(f['filesize']) + except: + pass cache_id = utils.get_md5('\n'.join([str(obj[i]) for i in obj if i[0] != '_'])) _cached_qualities[cache_id] = obj res[cache_id] = obj @@ -93,7 +116,7 @@ def _download_video_raw(url: str, quality_code : str, path: str, proxy: bool, ti # prepare arguments args = [ - utils.which('youtube-dl'), + utils.which('yt-dlp'), '--newline', '-f', data['format'], @@ -159,7 +182,7 @@ async def _download_video(url: str, quality_code : str, path: str, proxy: bool, except: return traceback.format_exc() -def _get_video_data_raw(path: str) -> dict | None: +def _get_video_data_raw(path: str, audio: bool) -> dict | None: ''' Get video duration, width, height and file size ''' try: # prepare arguments @@ -168,9 +191,9 @@ def _get_video_data_raw(path: str) -> dict | None: '-v', 'error', '-select_streams', - 'v:0', + 'a:0' if audio else 'v:0', '-show_entries', - 'stream=width,height,duration', + 'stream=duration' if audio else 'stream=width,height,duration', '-show_entries', 'format=size', '-of', @@ -188,24 +211,32 @@ def _get_video_data_raw(path: str) -> dict | None: try: txt = cp.stdout.decode(encoding='ascii').strip() except: - return None + txt = cp.stderr.decode(encoding='ascii').strip() + return txt parts = [i.strip() for i in txt.split('\n') if i.strip()] - # result - return { - 'width': int(parts[0]), - 'height': int(parts[1]), - 'duration': int(float(parts[2])), - 'size': int(parts[3]) - } + # result for audio + if audio: + return { + 'duration': int(float(parts[0])), + 'size': int(parts[1]) + } + # result for video + else: + return { + 'width': int(parts[0]), + 'height': int(parts[1]), + 'duration': int(float(parts[2])), + 'size': int(parts[3]) + } except subprocess.TimeoutExpired: return 'ffprobe timed out' except: return traceback.format_exc() -async def _get_video_data(path: str) -> dict | None: +async def _get_video_data(path: str, audio: bool) -> dict | None: ''' Async version ''' try: - return await asyncio.to_thread(_get_video_data_raw, path) + return await asyncio.to_thread(_get_video_data_raw, path, audio) except: return traceback.format_exc() @@ -220,7 +251,7 @@ def _generate_thumb_raw(video: str, timestamp: int, thumb: str) -> bool: '-i', video, '-vf', - 'thumbnail,scale=\'min(320,iw)\':\'min(320,ih)\':force_original_aspect_ratio=decrease', + 'thumbnail,scale=\'min(280,iw)\':\'min(280,ih)\':force_original_aspect_ratio=decrease', '-frames:v', '1', thumb @@ -254,6 +285,50 @@ async def _generate_thumb(video: str, timestamp: int, thumb: str) -> dict | None except: return traceback.format_exc() +def _download_thumb_for_tg_raw(url: str, proxy: bool, thumb: str) -> str | None: + ''' Downloads a thumbnail for Telegram ''' + try: + # prepare arguments + args = [ + utils.which('ffmpeg'), + '-i', + url, + '-vf', + 'thumbnail,scale=\'min(280,iw)\':\'min(280,ih)\':force_original_aspect_ratio=decrease', + thumb + ] + if proxy: + args = [utils.which('proxychains4')] + args + # start the process + cp = subprocess.run( + args, + capture_output=True, + timeout=10 + ) + # check the result + txt = None + try: + txt = cp.stdout.decode(encoding='ascii').strip() + txt = cp.stderr.decode(encoding='ascii').strip() + except: + pass + parts = [i.strip() for i in txt.split('\n') if i.strip()] + # result + if not os.path.isfile(thumb): + print('File does not exist somewhy') + return None + except subprocess.TimeoutExpired: + return 'ffprobe timed out' + except: + return traceback.format_exc() + return 'Wrong' + +async def _download_thumb_for_tg(url: str, proxy: bool, thumb: str) -> str | None: + ''' Async version ''' + try: + return await asyncio.to_thread(_download_thumb_for_tg_raw, url, proxy, thumb) + except: + return traceback.format_exc() async def mod_init(config: dict) -> bool: ''' Initialize the mod ''' @@ -284,7 +359,7 @@ def mod_get_mighty() -> bool: def mod_get_tags() -> None: ''' Get tags used by the mod ''' - return ['mvd', 'mvdl', 'mvdlp', 'mvdd', 'mvddp'] + return ['md', 'mvdl', 'mvdlp', 'madl', 'madlp', 'mdd', 'mddp'] async def mod_new_message(session, event) -> None: ''' Handle new message ''' @@ -292,8 +367,8 @@ async def mod_new_message(session, event) -> None: # get the message msg = event.message # not outgoing - do not process - if not msg.out: - return + #if not msg.out: + # return # peer must be user peer = msg.peer_id if type(peer) is not PeerUser: @@ -307,13 +382,14 @@ async def mod_new_message(session, event) -> None: args = args[1:] await asyncio.sleep(0.5) # help - if cmd == 'mvd': - response_text = 'mod_video_downloader:' + if cmd == 'md': + response_text = 'mod_audio_video_downloader:' response_text += '\n- mvdl[p] [URL] - get list of all video qualities' - response_text += '\n- mvdd[p] [CODE] - download video' + response_text += '\n- madl[p] [URL] - get list of all audio qualities' + response_text += '\n- mdd[p] [CODE] [TITLE] [§ PERFORMER] - download video or audio (for audio only: track title)' response_text += '\n\nUse \'p\' letter to utilize proxy' await event.reply(message=response_text) - # list qualities + # list video qualities elif cmd.startswith('mvdl'): if not args: await event.reply(message='No URL!') @@ -328,28 +404,63 @@ async def mod_new_message(session, event) -> None: result = 'Qualities:' for qid in qualities: data = qualities[qid] - result += '\n\nmvdd%s %s' % ('p' if cmd[-1] == 'p' else '', qid) - result += '\n- Format: %s' % data['format'] + # not a video + if data['is_audio']: + continue + # extensions to ignore + if data['ext'] in ['webm', 'mhtml']: + continue + result += '\n\nmdd%s %s' % ('p' if cmd[-1] == 'p' else '', qid) + result += '\n- Format name: %s' % data['format_name'] result += '\n- Extension: %s' % data['ext'] + if 'filesize' in data: + result += '\n- Filesize: %s KB' % (data['filesize'] / 1000) + await event.reply(message=result, parse_mode='HTML') + # list audio qualities + elif cmd.startswith('madl'): + if not args: + await event.reply(message='No URL!') + return + await event.reply(message='Checking URL... Please wait, you\'ll be notified if an error happens!') + qualities = await _get_all_qualities(' '.join(args), cmd[-1] == 'p') + # error + if type(qualities) is str: + await event.reply(message='Error:\n\n%s' % qualities) + return + # success + result = 'Qualities:' + for qid in qualities: + data = qualities[qid] + # not an audio + if not data['is_audio']: + continue + # extensions to ignore + if data['ext'] in ['webm']: + continue + result += '\n\nmdd%s %s' % ('p' if cmd[-1] == 'p' else '', qid) + result += '\n- Format name: %s' % data['format_name'] + result += '\n- Extension: %s' % data['ext'] + if 'filesize' in data: + result += '\n- Filesize: %s KB' % (data['filesize'] / 1000) await event.reply(message=result, parse_mode='HTML') # download - elif cmd.startswith('mvdd'): + elif cmd.startswith('mdd'): if not args: await event.reply(message='No CODE!') return # get the code and check it - code = args[-1] + code = args[0] if code not in _cached_qualities: await event.reply(message='This code does not exist. Use \'mvdl[p]\' to obtain the code.') return # get video data data = _cached_qualities[code] - await event.reply(message='Downloading the video... Please wait, you\'ll be notified if an error happens!') + await event.reply(message='Downloading the media... Please wait, you\'ll be notified if an error happens!') res = await _download_video(data['url'], code, 'mvd_temp/%s.bin' % code, cmd[-1] == 'p') # res is str - error if type(res) is str: utils.rm_glob('mvd_temp/%s.*' % code) - await event.reply(message='Failed to download video: %s' % res) + await event.reply(message='Failed to download media: %s' % res) return # res is false if not res: @@ -365,41 +476,82 @@ async def mod_new_message(session, event) -> None: pass except: utils.rm_glob('mvd_temp/%s.*' % code) - await event.reply(message='Failed to rename downloaded video') + await event.reply(message='Failed to rename downloaded media') return # get video data - video_data = await _get_video_data(new_name) + video_data = await _get_video_data(new_name, data['is_audio']) if type(video_data) is not dict: utils.rm_glob('mvd_temp/%s.*' % code) - await event.reply(message='Failed to use \'ffprobe\' to get video data') + await event.reply(message='Failed to use \'ffprobe\' to get media data:\n\n%s' % video_data) return - # generate the thumbnail - thumb_name = 'mvd_temp/%s.jpg' % code - if not await _generate_thumb(new_name, int(video_data['duration'] * 0.75), thumb_name): - utils.rm_glob('mvd_temp/%s.*' % code) - await event.reply(message='Failed to generate video thumbnail') - return - # log - await event.reply(message='Video is downloaded, thumbnail is generated, uploading it to Telegram...') - # send file - try: - await event.client.send_file( - entity=peer, - file=new_name, - caption='%s' % data['url'], - mime_type=utils.get_mime(data['ext']), - file_size=video_data['size'], - thumb=thumb_name, - supports_streaming=True, - attributes=[DocumentAttributeVideo( - duration=video_data['duration'], - w=video_data['width'], - h=video_data['height'], - supports_streaming=True - )] - ) - except: - await event.reply(message='Failed to upload video to telegram!') + # audio + if data['is_audio']: + # assign track title + remains = [i.strip() for i in ' '.join(args[1:]).split('§') if i.strip()] + title = data['title'] + author = data['author'] + thumbnail = data['thumbnail'] + # use title and author from user message + if remains: + title = remains[0] + if len(remains) > 1: + author = remains[-1] + # thumbnail exists, download it and change scale + if thumbnail: + thumb_path = 'mvd_temp/%s.jpg' % code + thumbnail = await _download_thumb_for_tg(thumbnail, cmd[-1] == 'p', thumb_path) + if thumbnail: + await event.reply(message='WARNING (audio is still being processed - THIS IS NOT AN ERROR). Failed to download thumbnail.\n\n%s' % thumbnail) + thumbnail = None + else: + thumbnail = thumb_path + # send file + await event.reply(message='Audio is downloaded, uploading it to Telegram...') + try: + await event.client.send_file( + entity=peer, + file=new_name, + caption='%s' % data['url'], + mime_type=utils.get_mime(data['ext']), + file_size=video_data['size'], + thumb=thumbnail, + attributes=[DocumentAttributeAudio( + duration=video_data['duration'], + title=title, + performer=author + )] + ) + except: + await event.reply(message='Failed to upload audio to telegram!\n\n%s' % traceback.format_exc()) + # video + else: + # generate the thumbnail + thumb_name = 'mvd_temp/%s.jpg' % code + if not await _generate_thumb(new_name, int(video_data['duration'] * 0.75), thumb_name): + utils.rm_glob('mvd_temp/%s.*' % code) + await event.reply(message='Failed to generate media thumbnail') + return + # log + await event.reply(message='Media is downloaded, thumbnail is generated, uploading it to Telegram...') + # send file + try: + await event.client.send_file( + entity=peer, + file=new_name, + caption='%s' % data['url'], + mime_type=utils.get_mime(data['ext']), + file_size=video_data['size'], + thumb=thumb_name, + supports_streaming=True, + attributes=[DocumentAttributeVideo( + duration=video_data['duration'], + w=video_data['width'], + h=video_data['height'], + supports_streaming=True + )] + ) + except: + await event.reply(message='Failed to upload media to telegram!\n\n%s' % traceback.format_exc()) utils.rm_glob('mvd_temp/%s.*' % code) except: utils.pex() diff --git a/utils.py b/utils.py index f279dc4..f3d8314 100644 --- a/utils.py +++ b/utils.py @@ -36,7 +36,7 @@ def get_all_mods() -> list[str]: def get_md5(data: str) -> str: ''' Returns MD5 for data ''' md5_hash = hashlib.md5() - md5_hash.update(str(data).encode('ascii')) + md5_hash.update(str(data).encode('ascii', errors='ignore')) return md5_hash.hexdigest()