version = (1, 4, 0) # meta developer: @RUIS_VlP, @RoKrz # requires: yt_dlp import yt_dlp import uuid import os import re import tempfile from .. import loader, utils def extract_video_link(text): """Извлекает видео с сайтов""" if not text: return None video_sites_patterns = [ # YouTube r"(https?://)?(www\.)?(youtube\.com|youtu\.be)/[^\s]+", # Social Media r"(https?://)?(www\.)?(tiktok\.com|vt\.tiktok\.com|vm\.tiktok\.com)/[^\s]+", r"(https?://)?(www\.)?instagram\.com/(p|reel|tv)/[^\s]+", r"(https?://)?(www\.)?(twitter\.com|x\.com)/[^\s]+/status/[^\s]+", r"(https?://)?(www\.)?facebook\.com/[^\s]+/videos/[^\s]+", r"(https?://)?(www\.)?reddit\.com/r/[^\s]+/comments/[^\s]+", # Video Platforms r"(https?://)?(www\.)?vimeo\.com/[^\s]+", r"(https?://)?(www\.)?dailymotion\.com/video/[^\s]+", r"(https?://)?(www\.)?twitch\.tv/(videos/|clip/|[^/]+$)[^\s]*", r"(https?://)?(www\.)?streamable\.com/[^\s]+", # News & Media r"(https?://)?(www\.)?bbc\.co\.uk/iplayer/[^\s]+", r"(https?://)?(www\.)?cnn\.com/videos/[^\s]+", r"(https?://)?(www\.)?reuters\.com/video/[^\s]+", # Educational r"(https?://)?(www\.)?coursera\.org/learn/[^\s]+", r"(https?://)?(www\.)?udemy\.com/course/[^\s]+", r"(https?://)?(www\.)?khanacademy\.org/[^\s]+", # Russian platforms r"(https?://)?(www\.)?rutube\.ru/video/[^\s]+", r"(https?://)?(www\.)?vk\.com/(video|clip)[^\s]+", r"(https?://)?(www\.)?ok\.ru/video/[^\s]+", # Other popular platforms r"(https?://)?(www\.)?pornhub\.com/view_video\.php\?viewkey=[^\s]+", r"(https?://)?(www\.)?xvideos\.com/video[^\s]+", r"(https?://)?(www\.)?soundcloud\.com/[^\s]+", r"(https?://)?(www\.)?bandcamp\.com/track/[^\s]+", r"(https?://)?(www\.)?mixcloud\.com/[^\s]+", # Live streaming r"(https?://)?(www\.)?periscope\.tv/[^\s]+", r"(https?://)?(www\.)?ustream\.tv/[^\s]+", # International r"(https?://)?(www\.)?bilibili\.com/video/[^\s]+", r"(https?://)?(www\.)?niconico\.jp/watch/[^\s]+", r"(https?://)?(www\.)?youku\.com/v_show/[^\s]+", # Generic fallback for other video URLs r"https?://[^\s]+\.(mp4|webm|avi|mkv|mov|flv|m4v)", ] for pattern in video_sites_patterns: match = re.search(pattern, text, re.IGNORECASE) if match: return match.group(0) general_url_pattern = r"https?://[^\s]+" match = re.search(general_url_pattern, text) if match: url = match.group(0) excluded_domains = [ 'google.com', 'yandex.ru', 'wikipedia.org', 'github.com', 'stackoverflow.com', 'reddit.com/r/', 'amazon.com' ] if not any(domain in url.lower() for domain in excluded_domains): return url return None async def download_video(url, cookies_text=None, youtube_client="default", custom_user_agent=None): output_dir = utils.get_base_dir() random_uuid = str(uuid.uuid4()) os.makedirs(output_dir, exist_ok=True) formats_to_try = [ 'best[ext=mp4]', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]', 'bestvideo+bestaudio/best', 'best', 'best*', 'bestvideo+bestaudio', 'best[height<=1080]', 'best[height<=720]', 'worst', 'worst*', ] cookies_file = None if cookies_text and cookies_text.strip(): cleaned_cookies = cookies_text.strip() if cleaned_cookies.startswith('"') or cleaned_cookies.startswith("'"): cleaned_cookies = cleaned_cookies[1:] if cleaned_cookies.endswith('"') or cleaned_cookies.endswith("'"): cleaned_cookies = cleaned_cookies[:-1] cookies_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt', encoding='utf-8') cookies_file.write(cleaned_cookies) cookies_file.close() try: is_youtube = 'youtube.com' in url.lower() or 'youtu.be' in url.lower() for format_option in formats_to_try: ydl_opts = { 'format': format_option, 'outtmpl': os.path.join(output_dir, f'{random_uuid}.%(ext)s'), 'noplaylist': True, 'merge_output_format': 'mp4', } if cookies_file: ydl_opts['cookiefile'] = cookies_file.name if custom_user_agent: ydl_opts['http_headers'] = {'User-Agent': custom_user_agent} if is_youtube and youtube_client != "default": ydl_opts['extractor_args'] = {'youtube': {'player_client': [youtube_client]}} try: with yt_dlp.YoutubeDL(ydl_opts) as ydl: info_dict = ydl.extract_info(url, download=True) video_ext = info_dict.get('ext', None) file_path = os.path.join(output_dir, f"{random_uuid}.{video_ext}") title = info_dict.get('title', None) channel = info_dict.get('uploader', None) return file_path, title, channel except Exception as e: if "Requested format is not available" in str(e) or "No video formats found" in str(e): continue else: raise e raise Exception("Не удалось скачать видео ни в одном формате") finally: if cookies_file: try: os.unlink(cookies_file.name) except: pass def convert_markdown_to_html(template: str, link: str) -> str: return re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'\1', template).replace("{link}", link) @loader.tds class YouTube_DLDMod(loader.Module): """Помогает скачивать видео с YouTube, TikTok и др.""" strings = { "name": "YouTube-DLD", "no_link": "❌ Пожалуйста, укажите ссылку на видео либо ответьте на сообщение с ней.", "default_downloading": "📥 Начинаю загрузку видео.\n\nℹ️ Это может занять до 5 минут, в зависимости от длины и качества видео.", "default_error": "❌ Ошибка!\n\n{}", "default_response": "🎥 Вот [ваше видео]({link})!\n\n{title}", "default_channel": "📺 Канал: {channel}", "cookies_error": "🍪 YouTube требует аутентификацию!\n\n❌ Ошибка: Sign in to confirm you're not a bot\n\nВозможные причины:\n▫️ YouTube детектит запросы без аутентификации\n▫️ IP сервера может быть заблокирован YouTube\n▫️ Видео требует подтверждения возраста/входа\n\nРешения (попробуй по порядку):\n\n1️⃣ Смени YouTube клиент:\n• Открой .cfg YouTube-DLD\n• Попробуй разные значения youtube_client:\n - mweb (мобильная веб-версия, часто работает)\n - android (Android приложение, сработало на сервере во Франции)\n - ios (iOS приложение)\n - tv_embedded (встроенный ТВ плеер)\n\n2️⃣ Добавь куки (для пользователей из проблемных регионов):\n• Открой НОВОЕ приватное окно (в браузере ctrl+shift+N) и залогинься на YouTube\n• Перейди на https://www.youtube.com/robots.txt в ТОЙ же вкладке\n• Cookie-Editor (расширение) → Export → Netscape format\n• СРАЗУ закрой приватное окно\n• Вставь куки в youtube_cookies (БЕЗ кавычек)", "supported_sites": """🎥 Поддерживаемые сайты: 🔴 YouTube — youtube.com, youtu.be 🎵 TikTok — tiktok.com, vt.tiktok.com, vm.tiktok.com 📸 Instagram — instagram.com (посты, reels, IGTV) 🐦 X (Twitter) — x.com, twitter.com 👥 Facebook — facebook.com (видео) 🎬 Vimeo — vimeo.com 📺 Twitch — twitch.tv (стримы, клипы, VOD) 🤖 Reddit — reddit.com (видео из постов) ⚡ Dailymotion — dailymotion.com 🇷🇺 Российские: ▫️ RuTube — rutube.ru ▫️ ВКонтакте — vk.com (видео) ▫️ Одноклассники — ok.ru 📚 Образовательные: ▫️ Coursera — coursera.org ▫️ Udemy — udemy.com ▫️ Khan Academy — khanacademy.org 🌍 Международные: ▫️ Bilibili — bilibili.com ▫️ NicoNico — niconico.jp ▫️ BBC iPlayer — bbc.co.uk/iplayer 🎵 Аудио: ▫️ SoundCloud — soundcloud.com ▫️ Bandcamp — bandcamp.com ▫️ Mixcloud — mixcloud.com И многие другие платформы...""" } def __init__(self): self.config = loader.ModuleConfig( loader.ConfigValue( "show_link", True, "Показывать ссылку в сообщении?", validator=loader.validators.Boolean(), ), loader.ConfigValue( "downloading_text", self.strings["default_downloading"], "Текст во время загрузки" ), loader.ConfigValue( "error_text", self.strings["default_error"], "Текст ошибки. (используй {} для ошибки)" ), loader.ConfigValue( "response_text", self.strings["default_response"], "Ответ после загрузки. (используй {link} для ссылки и {title} для названия видео)" ), loader.ConfigValue( "show_channel", True, "Показывать название канала?", validator=loader.validators.Boolean(), ), loader.ConfigValue( "channel_text", self.strings["default_channel"], "Текст для отображения канала. (используй {channel} для имени канала)" ), loader.ConfigValue( "youtube_cookies", "", "🍪 Куки YouTube в формате Netscape (опционально)\n\n" "⚠️ ВНИМАНИЕ: Риск бана аккаунта! Используй тестовый аккаунт.\n\n" "Как получить:\n" "1. НОВОЕ приватное окно (Ctrl+Shift+N) → залогинься на YouTube\n" "2. Перейди на https://www.youtube.com/robots.txt в той же вкладке\n" "3. Cookie-Editor (расширение) → Export → Netscape format\n" "4. СРАЗУ закрой приватное окно\n" "5. Вставь текст сюда (БЕЗ кавычек)", validator=loader.validators.String(), ), loader.ConfigValue( "youtube_client", "mweb", "📱 YouTube клиент для обхода блокировок\n\n" "Доступные варианты:\n" "• default - стандартный (может не работать)\n" "• mweb - мобильная веб-версия\n" "• android - Android приложение (рекомендуется)\n" "• ios - iOS приложение\n" "• tv_embedded - встроенный ТВ плеер\n\n" "Если видео не скачивается, попробуй другой клиент!", validator=loader.validators.Choice(["default", "mweb", "android", "ios", "tv_embedded"]), ), loader.ConfigValue( "custom_user_agent", "", "🌐 Кастомный User-Agent (опционально)\n\n" "Можно указать User-Agent браузера для обхода некоторых блокировок.\n" "Например:\n" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\n\n" "Оставь пустым для использования стандартного.", validator=loader.validators.String(), ), ) @loader.command() async def dvlist(self, message): """Показать список всех поддерживаемых сайтов""" await utils.answer(message, self.strings["supported_sites"]) @loader.command() async def dlvideo(self, message): """<ссылка> или ответ на сообщение со ссылкой — скачивает видео с поддерживаемых платформ""" args = utils.get_args_raw(message) reply = await message.get_reply_message() link = extract_video_link(args) if args else None if not link and reply: link = extract_video_link(reply.raw_text) if not link: await utils.answer(message, self.strings["no_link"]) return await utils.answer(message, self.config["downloading_text"]) try: cookies_text = self.config["youtube_cookies"].strip() if self.config["youtube_cookies"] else None youtube_client = self.config["youtube_client"] user_agent = self.config["custom_user_agent"].strip() if self.config["custom_user_agent"] else None video, title, channel = await download_video(link, cookies_text, youtube_client, user_agent) if self.config["show_link"]: caption_template = self.config["response_text"] caption = convert_markdown_to_html(caption_template, link) caption = caption.replace("{title}", title or "") if self.config["show_channel"] and channel: channel_text = self.config["channel_text"].replace("{channel}", channel) caption += f"\n\n{channel_text}" else: caption = title or "Готово!" await utils.answer_file( message, video, caption=caption, parse_mode="HTML", reply_to=reply or message, silent=True ) try: await message.delete() except: pass try: os.remove(video) except: pass except Exception as e: error_str = str(e) if "Sign in to confirm you're not a bot" in error_str or "Use --cookies" in error_str: await utils.answer(message, self.strings["cookies_error"]) else: error_msg = self.config["error_text"].format(e) await utils.answer(message, error_msg) try: if 'video' in locals(): os.remove(video) except: pass