Added basic downloading ability

This commit is contained in:
2026-09-07 03:23:03 +03:00
parent a71d4ce5f8
commit 1077fc1d5f
4 changed files with 89 additions and 9 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.venv/ .venv/
session_storage/ session_storage/
__pycache__/ __pycache__/
temp/
*.swp *.swp
*.swo *.swo
*.vscode *.vscode

View File

@@ -8,7 +8,7 @@ Matrix бот для скачивания видео/аудио с *YouTube, Pin
тестировать библиотеку [`mab`](https://git.tyukalov.su/nikita/mab), а точнее тестировать библиотеку [`mab`](https://git.tyukalov.su/nikita/mab), а точнее
её возможности загрузки вложений в Matrix. её возможности загрузки вложений в Matrix.
> В системе должны быть установлены `ffmpeg` и `ffprobe`. > В системе должны быть установлены `ffmpeg`, `ffprobe` и `yt-dlp`.
## Работа с ботом ## Работа с ботом
@@ -23,6 +23,8 @@ Matrix бот для скачивания видео/аудио с *YouTube, Pin
бот отправит медиафайлы из папки `test_media` бот отправит медиафайлы из папки `test_media`
- Если сообщение начинается не с `http://` или `https://`, то бот даст справку - Если сообщение начинается не с `http://` или `https://`, то бот даст справку
- Если сообщение начинается с `http://` или `https://`, то бот передаст текст - Если сообщение начинается с `http://` или `https://`, то бот передаст текст
сообщения в качестве ссылки для `yt-dlp` сообщения в качестве ссылки `yt-dlp`, скачает видео в максимально возможном
качестве, и отправит его в Matrix
**Дальнейший интерфейс ещё не придуман** **Интерфейс планируется добработать, в том числе добавить возможность скачивать
аудио, копировать cookies из браузера, выбирать качество.**

50
downloader.py Normal file
View File

@@ -0,0 +1,50 @@
import asyncio
import tempfile
import signal
import os
from pathlib import Path
from dataclasses import dataclass
class Downloader:
"""Class that performs content download."""
#
# PUBLIC
#
def __init__(self, url: str):
self._url = url
async def download_video(self, directory: str | Path) -> Path:
directory = Path(directory).resolve()
directory.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(dir=directory, prefix=".ytdlp-") as tmp:
proc = await asyncio.create_subprocess_exec(
"yt-dlp",
"-f", "bv*+ba/b",
"-P", f"home:{directory}",
"-P", f"temp:{tmp}",
"--no-playlist",
"--print", "after_move:filepath",
self._url,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
start_new_session=True,
)
try:
stdout, stderr = await proc.communicate()
except asyncio.CancelledError:
try:
os.killpg(proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
await asyncio.shield(proc.wait())
raise
if proc.returncode != 0:
raise RuntimeError(stderr.decode(errors="replace"))
return Path(stdout.decode().strip())

View File

@@ -3,6 +3,8 @@
import asyncio import asyncio
import glob import glob
import html import html
import os
import shutil
import traceback import traceback
from mab import MatrixBot from mab import MatrixBot
@@ -11,10 +13,35 @@ from mab.filters.text import TextFilter, TextStartsWithFilter, TextEndsWithFilte
from nio import MatrixRoom, RoomMessageText from nio import MatrixRoom, RoomMessageText
from downloader import Downloader
# #
# PRIVATE DATA # PRIVATE DATA
# #
_bot: MatrixBot _bot: MatrixBot
_tasks: set[asyncio.Task] = set()
#
# PRIVATE
#
async def worker_download(room: MatrixRoom, text: RoomMessageText) -> None:
# respond
response = (
f"<strong>Скачивание видео по "
f"<a href=\"{html.escape(text.body)}\">вашей ссылке</a></strong>"
)
await _bot.send_text_to_room(room.room_id, response, True)
# get formats
d = Downloader(text.body)
result = await d.download_video("temp")
await _bot.send_text_to_room(room.room_id, "<strong>Скачано, загрузка в Matrix...</strong>", True)
# upload
try:
await _bot.send_video_to_room(room.room_id, result, caption=f"Скачано из {text.body}")
except:
await _bot.send_text_to_room(room.room_id, traceback.format_exc(), False)
finally:
os.unlink(result)
# #
# CALLBACKS # CALLBACKS
@@ -33,9 +60,9 @@ async def command_http_https(room: MatrixRoom, text: RoomMessageText) -> None:
"""The message starts with `http://` or `https://`""" """The message starts with `http://` or `https://`"""
if text.sender == _bot.get_client().user_id: if text.sender == _bot.get_client().user_id:
return return
resp = "<strong>Сообщение начинается с <code>http://</code> или с <code>" t = asyncio.create_task(worker_download(room, text))
resp += "https://</code></strong>. Ещё не реализовано." _tasks.add(t)
await _bot.send_text_to_room(room.room_id, resp, True) t.add_done_callback(_tasks.discard)
async def command_test(room: MatrixRoom, text: RoomMessageText) -> None: async def command_test(room: MatrixRoom, text: RoomMessageText) -> None:
"""The message ends with ` TesT` (case matters)""" """The message ends with ` TesT` (case matters)"""
@@ -83,14 +110,14 @@ async def command_test(room: MatrixRoom, text: RoomMessageText) -> None:
await _bot.send_text_to_room(room.room_id, traceback.format_exc(), False) await _bot.send_text_to_room(room.room_id, traceback.format_exc(), False)
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
# #
# PUBLIC # PUBLIC
# #
async def initialize(bot: MatrixBot) -> None: async def initialize(bot: MatrixBot) -> None:
"""This function initializes the bot. It does not start it.""" """This function initializes the bot. It does not start it."""
global _bot global _bot
shutil.rmtree("temp", ignore_errors=True)
os.makedirs("temp", exist_ok=True)
_bot = bot _bot = bot
bot.add_event_callback(command_http_https, TextStartsWithFilter(["https://", "http://"])) bot.add_event_callback(command_http_https, TextStartsWithFilter(["https://", "http://"]))
bot.add_event_callback(command_test, TextEndsWithFilter(" TesT", any_case=False)) bot.add_event_callback(command_test, TextEndsWithFilter(" TesT", any_case=False))
@@ -98,4 +125,4 @@ async def initialize(bot: MatrixBot) -> None:
async def deinitizalize(bot: MatrixBot) -> None: async def deinitizalize(bot: MatrixBot) -> None:
"""This function deinitializes the bot. It does not stop it.""" """This function deinitializes the bot. It does not stop it."""
pass shutil.rmtree("temp", ignore_errors=True)