Files
downloads-all-bot/bot/downloader.py
2026-02-18 23:30:11 +07:00

322 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# downloader.py
import yt_dlp
import threading
import os
import logging
import subprocess
import shutil
import asyncio
from typing import List
from config import (
DEFAULT_TIKTOK_VIDEO_QUALITY,
DEFAULT_TIKTOK_AUDIO_QUALITY,
DEFAULT_INSTAGRAM_VIDEO_QUALITY,
)
logger = logging.getLogger(__name__)
class DownloadCancelled(Exception):
pass
def _progress_hook(cancel_event, progress_cb):
def hook(d):
if cancel_event.is_set():
raise DownloadCancelled("Cancelled by user")
if d["status"] in ["downloading", "finished"] and progress_cb:
progress_cb(d)
return hook
def optimize_for_telegram(input_path: str, output_path: str) -> None:
"""
Синхронная функция: конвертирует видео в Telegram-safe mp4
"""
file_size_mb = os.path.getsize(input_path) / (1024 * 1024)
crf = 28 if file_size_mb > 50 else 23
cmd = [
'ffmpeg',
'-i', input_path,
'-c:v', 'libx264',
'-preset', 'fast',
'-crf', str(crf),
'-c:a', 'aac',
'-b:a', '128k',
'-movflags', '+faststart',
'-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
'-y',
output_path
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
if result.returncode != 0:
logger.error(f"FFmpeg error: {result.stderr}")
shutil.copy2(input_path, output_path)
# ---------------- YOUTUBE/VK: Video (с выбором качества) ------
def download_video(
url: str,
quality: str,
out_path: str,
cookies: str | None,
cancel_event: threading.Event,
progress_cb,
):
ydl_opts = {
"format": f"best[height<={quality}]/best",
"outtmpl": out_path,
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
"quiet": True,
"no_warnings": True,
"concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
# Ищем реальный файл
base = out_path.rsplit('.', 1)[0]
actual_video = None
for ext in ['', '.mp4', '.mov', '.webm', '.mkv']:
candidate = base + ext
if os.path.exists(candidate):
actual_video = candidate
break
if not actual_video:
raise Exception(f"Video file not created for {url}")
# Конвертируем в Telegram-safe mp4 (синхронно)
optimized_path = base + "_telegram.mp4"
optimize_for_telegram(actual_video, optimized_path) # ← без await!
# Удаляем старый файл и ставим конвертированный
for path in [out_path, actual_video]:
if os.path.exists(path): os.remove(path)
os.rename(optimized_path, out_path)
# ---------------- AUDIO (YouTube/VK) ------
def download_audio(
url: str,
out_path: str,
cookies: str | None,
cancel_event: threading.Event,
progress_cb,
):
ydl_opts = {
"format": "bestaudio/best",
"outtmpl": out_path.replace('.mp3', ''),
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
"postprocessors": [{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
"preferredquality": "192",
}],
"quiet": True,
"no_warnings": True,
"concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
# Ищем файл
base = out_path.rsplit('.', 1)[0]
actual_audio = None
for ext in ['', '.mp3', '.m4a', '.opus', '.weba']:
candidate = base + ext
if os.path.exists(candidate):
actual_audio = candidate
break
if not actual_audio:
raise Exception(f"Audio file not created for {url}")
# Конвертируем в mp3
if actual_audio != out_path:
optimized = base + "_telegram.mp3"
optimize_for_telegram_audio(actual_audio, optimized) # ← отдельная функция
if os.path.exists(out_path): os.remove(out_path)
os.rename(optimized, out_path)
# ---------------- TikTok: Video + Audio (автоматически) ------
def download_tiktok_video_and_audio(
url: str,
video_path: str,
audio_path: str,
cookies: str | None,
cancel_event: threading.Event,
progress_cb,
):
# Скачиваем видео
video_opts = {
"format": f"bestvideo[height<={DEFAULT_TIKTOK_VIDEO_QUALITY}]+bestaudio/best",
"outtmpl": video_path,
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
"quiet": True,
"no_warnings": False,
"concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(video_opts) as ydl:
ydl.download([url])
# Ищем видеофайл
base_video = video_path.rsplit('.', 1)[0]
actual_video = None
for ext in ['', '.mp4', '.mov', '.webm']:
candidate = base_video + ext
if os.path.exists(candidate):
actual_video = candidate
break
if not actual_video:
raise Exception("TikTok video file not created")
# Конвертируем в Telegram-safe mp4
optimized_video = base_video + "_telegram.mp4"
optimize_for_telegram(actual_video, optimized_video)
for path in [video_path, actual_video]:
if os.path.exists(path): os.remove(path)
os.rename(optimized_video, video_path)
# Аудио
audio_opts = {
"format": "bestaudio/best",
"outtmpl": audio_path.replace('.mp3', ''),
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
"postprocessors": [{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
"preferredquality": DEFAULT_TIKTOK_AUDIO_QUALITY,
}],
"quiet": True,
"no_warnings": False,
"concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(audio_opts) as ydl:
ydl.download([url])
base_audio = audio_path.rsplit('.', 1)[0]
actual_audio = None
for ext in ['', '.mp3', '.m4a', '.opus']:
candidate = base_audio + ext
if os.path.exists(candidate):
actual_audio = candidate
break
if not actual_audio:
raise Exception("TikTok audio file not created")
if actual_audio != audio_path:
optimized_audio = base_audio + "_telegram.mp3"
optimize_for_telegram_audio(actual_audio, optimized_audio)
if os.path.exists(audio_path): os.remove(audio_path)
os.rename(optimized_audio, audio_path)
# ---------------- Instagram: Video (1080p) ------
def download_instagram_video(
url: str,
out_path: str,
cookies: str | None,
cancel_event: threading.Event,
progress_cb,
):
ydl_opts = {
"format": f"bestvideo[height<={DEFAULT_INSTAGRAM_VIDEO_QUALITY}]+bestaudio/best",
"outtmpl": out_path,
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
"quiet": True,
"no_warnings": False,
"concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
base = out_path.rsplit('.', 1)[0]
actual = None
for ext in ['', '.mp4', '.mov', '.webm']:
candidate = base + ext
if os.path.exists(candidate):
actual = candidate
break
if not actual:
raise Exception("Instagram video not created")
optimized = base + "_telegram.mp4"
optimize_for_telegram(actual, optimized)
if os.path.exists(out_path): os.remove(out_path)
os.rename(optimized, out_path)
# ---------------- Audio helper (синхронная) ------
def optimize_for_telegram_audio(input_path: str, output_path: str) -> None:
"""Конвертирует аудио в Telegram-safe mp3"""
cmd = [
'ffmpeg',
'-i', input_path,
'-c:a', 'aac',
'-b:a', '192k',
'-loglevel', 'error',
'-y',
output_path
]
subprocess.run(cmd, capture_output=True, text=True, timeout=600)
# ---------------- Playlist ------
def download_playlist_videos(
playlist_info: dict,
output_dir: str,
cookies: str | None,
cancel_event: threading.Event,
progress_cb,
) -> List[str]:
downloaded_files = []
ydl_opts = {
"format": "best[height<=1080]/best",
"outtmpl": os.path.join(output_dir, "%(title)s [%(id)s].%(ext)s"),
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
"quiet": True,
"no_warnings": True,
"ignoreerrors": True,
"extract_flat": False,
"nooverwrites": True,
"concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760,
}
try:
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
entries = playlist_info.get('entries', [])
for i, entry in enumerate(entries, 1):
if cancel_event.is_set():
raise DownloadCancelled("Cancelled by user")
if not entry.get('url'):
continue
try:
info = ydl.extract_info(entry['url'], download=True)
if info:
filename = ydl.prepare_filename(info)
if os.path.exists(filename):
downloaded_files.append(filename)
except Exception as e:
logger.error(f"Error downloading video {i}: {e}")
return downloaded_files
except Exception as e:
logger.error(f"Playlist download error: {e}")
raise