# █▀▀▄ █▀▄▀█ █▀█ █▀▄ █▀
# ▀▀▀█ ▄ █ ▀ █ █▄█ █▄▀ ▄█
# #### Copyright (c) 2026 Archquise #####
# 💬 Contact: https://t.me/archquise
# 🔒 Licensed under the GNU AGPLv3.
# 📄 LICENSE: https://raw.githubusercontent.com/archquise/Q.Mods/main/LICENSE
# ---------------------------------------------------------------------------------
# Name: YTDL
# Description: Downloads and sends audio/video from YouTube
# Author: @quise_m
# ---------------------------------------------------------------------------------
# meta developer: @quise_m
# meta banner: https://raw.githubusercontent.com/archquise/qmods_meta/main/ytdl.png
# requires: yt_dlp ffmpeg
# ---------------------------------------------------------------------------------
import logging
import os
import platform
import re
import shutil
import zipfile
from http import HTTPStatus
import aiofiles
import aiohttp
from yt_dlp import YoutubeDL
from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds
class YTDLMod(loader.Module):
"""Downloads and sends audio/video from YouTube."""
strings = { # noqa: RUF012
"name": "YTDL",
"_cls_doc": "Downloads and sends audio/video from YouTube",
"invalid_args": "❌ There is no arguments or they are invalid", # noqa: E501
"downloading": "🕐 Downloading...", # noqa: E501
"done": "✅ Done!",
"cookie_desc": "Cookie account (helps downloading video with strict age rating restricrions)", # noqa: E501
"deno_err": '❗️ Error! The Deno JavaScript engine was not install automatically.\nThis is a required dependency for yt-dlp (a library for downloading video/audio) to work correctly.\n\nTo continue, you need to install the engine manually, or resolve any issues preventing automatic installation and restart the userbot.', # noqa: E501
"err": "❗️ Error!\n\nAdditional info: {}", # noqa: E501
}
strings_ru = { # noqa: RUF012
"_cls_doc": "Скачивает и отправляет аудио/видео с Ютуба",
"invalid_args": "❌ Нет аргументов или они неверны", # noqa: E501
"downloading": "🕐 Скачиваю...",
"done": "✅ Готово!",
"cookie_desc": "Куки аккаунта (помогает скачивать видео с жесткими возрастными ограничениями)", # noqa: E501, RUF001
"deno_err": '❗️ Ошибка! JS-движок Deno не установился автоматически.\nЭто необходимая зависимость для корректной работы yt-dlp (библиотека для скачивания видео/аудио).\n\nДля продолжения вам необходимо установить движок вручную, или устранить препятствия для автоматической установки и перезагрузить юзербота.', # noqa: E501
"err": "❗️ Ошибка!\n\nДоп.информация: {}", # noqa: E501, RUF001
}
deno_error = (
"Deno wasn't installed in auto-mode.",
"Please, install it manually or resolve the issue and reboot userbot.",
)
def _validate_url(self, url: str) -> bool:
"""Validate URL format."""
if not url:
return False
url_pattern = re.compile(
r"^(?:https?://)?(?:www\.|m\.)?(?:youtube\.com|youtu\.be|music\.youtube\.com)/(?:watch\?v=|playlist\?list=|channel/|@|live/|shorts/)?[\w-]+",
re.IGNORECASE,
)
return url_pattern.match(url) is not None
async def get_target(self) -> str:
"""Check OS and processor architecture and return right postfix."""
system = platform.system()
machine = platform.machine().lower()
if system == "Windows":
return "Windows"
if system == "Darwin":
return (
"aarch64-apple-darwin" if machine == "arm64" else "x86_64-apple-darwin"
)
if system == "Linux":
return (
"aarch64-unknown-linux-gnu"
if machine in ("aarch64", "arm64")
else "x86_64-unknown-linux-gnu"
)
return "x86_64-unknown-linux-gnu"
def _get_deno(self) -> str | None:
if not (source := self.get("deno_source")) or source == "install_failed" or not os.path.exists(source):
logger.critical(self.deno_error)
return None
return source
def __init__(self): # noqa: ANN204, D107
self.config = loader.ModuleConfig(
loader.ConfigValue(
"youtube_cookie",
None,
lambda: self.strings["cookie_desc"],
validator=loader.validators.Hidden(),
),
)
async def client_ready(self, client, db): # noqa: ANN001, ANN201, D102, ARG002
deno_which = shutil.which("deno", path=os.environ.get("PATH", "") + os.pathsep + os.getcwd()) # noqa: E501
if deno_which:
self.set("deno_source", deno_which)
return
logger.warning("Deno is not installed, attempting installation...")
target = await self.get_target()
if target == "Windows":
logger.critical(
"Windows platform is unsupported, please, unload the module.",
)
return
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(20)) as session:
download_link = f"https://github.com/denoland/deno/releases/latest/download/deno-{target}.zip"
async with session.get(download_link) as resp:
if resp.status == HTTPStatus.OK:
async with aiofiles.open("deno.zip", mode="wb") as f:
async for chunk in resp.content.iter_chunked(8192):
await f.write(chunk)
else:
logger.critical("Failed to download Deno: HTTP %s", resp.status)
self.set("deno_source", "install_failed")
return
if os.path.exists('deno.zip'):
with zipfile.ZipFile("deno.zip", "r") as zip_ref:
zip_ref.extractall()
os.remove('deno.zip')
os.chmod(path=os.path.join(os.getcwd(), "deno"), mode=0o755)
self.set("deno_source", os.path.join(os.getcwd(), "deno"))
return
@loader.command(en_doc="Download video", ru_doc="Скачать видео")
async def ytdlvcmd(self, message): # noqa: ANN001, ANN201, D102
args = utils.get_args(message)
if not args or not self._validate_url(args[0]) or len(args) > 1:
await utils.answer(message, self.strings["invalid_args"])
return
if not (source := self._get_deno()):
await utils.answer(message, self.strings["deno_err"])
return
await utils.answer(message, self.strings["downloading"])
filename_prefix = f"video_{message.id}"
ydl_opts = {
"quiet": True,
"outtmpl": f"{filename_prefix}.%(ext)s",
"js_runtimes": {"deno": {"path": source}},
"format": "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best",
"merge_output_format": "mp4",
}
if cookie := self.get("youtube_cookie"):
ydl_opts["cookiefile"] = cookie
try:
with YoutubeDL(ydl_opts) as ydl:
info = await utils.run_sync(lambda: ydl.extract_info(args[0], download=True))
filename = ydl.prepare_filename(info)
await utils.answer(
message,
self.strings["done"],
file=filename,
invert_media=True,
)
os.remove(filename)
except Exception as e:
logger.exception("Catched error during download!")
await utils.answer(self.strings["err"].format(e))
@loader.command(en_doc="Download audio", ru_doc="Скачать аудио")
async def ytdlacmd(self, message): # noqa: ANN001, ANN201, D102
args = utils.get_args(message)
if not args or not self._validate_url(args[0]) or len(args) > 1:
await utils.answer(message, self.strings["invalid_args"])
return
if not (source := self._get_deno()):
await utils.answer(message, self.strings["deno_err"])
return
await utils.answer(message, self.strings["downloading"])
ydl_opts = {
"quiet": True,
"outtmpl": f"audio_{message.id}.%(ext)s",
"js_runtimes": {"deno": {"path": source}},
"format": "bestaudio/best",
"postprocessors": [
{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
"preferredquality": "0",
},
{
"key": "FFmpegMetadata",
"add_metadata": True,
},
{
"key": "EmbedThumbnail",
},
],
"writethumbnail": True,
}
if cookie := self.get("youtube_cookie"):
ydl_opts["cookiefile"] = cookie
try:
with YoutubeDL(ydl_opts) as ydl:
info = await utils.run_sync(lambda: ydl.extract_info(args[0], download=True))
filename = os.path.splitext(ydl.prepare_filename(info))[0] + ".mp3"
await utils.answer(message, self.strings["done"], file=filename)
os.remove(filename)
except Exception as e:
logger.exception("Catched error during download!")
await utils.answer(self.strings["err"].format(e))