Files
downloads-all-bot/bot/downloader.py
2026-02-18 14:05:36 +07:00

337 lines
9.8 KiB
Python

# downloader.py
import yt_dlp
import threading
import os
import logging
import shutil
from typing import List, Optional
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
# ---------------- STANDARD VIDEO (YouTube/VK) --------------
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,
"merge_output_format": "mp4",
"cookiefile": cookies,
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
"quiet": True,
"no_warnings": True,
"extractor_args": {
"tiktok": {"skip_impersonation": True},
"instagram": {"skip_impersonation": True},
},
"concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
# ---------------- TikTok: Видео + Аудио (автоматически) --------------
def download_tiktok_video_and_audio(
url: str,
video_path: str,
audio_path: str,
cookies: str | None,
cancel_event: threading.Event,
progress_cb,
):
"""
Скачивает TikTok: видео (1080p) + аудио (MP3 192kbps).
Возвращает (video_path, audio_path) или None.
"""
# 1. Скачиваем видео в 1080p
video_opts = {
"format": f"bestvideo[height<={DEFAULT_TIKTOK_VIDEO_QUALITY}]+bestaudio/best",
"outtmpl": video_path.replace('.mp4', ''),
"merge_output_format": "mp4",
"cookiefile": cookies,
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
"quiet": True,
"no_warnings": True,
"extractor_args": {"tiktok": {"skip_impersonation": True}},
"concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(video_opts) as ydl:
ydl.download([url])
# Проверяем, что видео создано
if not os.path.exists(video_path):
raise Exception("TikTok video file not created")
# 2. Извлекаем аудио в MP3
audio_opts = {
"format": "bestaudio/best",
"outtmpl": audio_path.replace('.mp3', ''),
"cookiefile": cookies,
"postprocessors": [{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
"preferredquality": DEFAULT_TIKTOK_AUDIO_QUALITY,
}],
"quiet": True,
"no_warnings": True,
"extractor_args": {"tiktok": {"skip_impersonation": True}},
"concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(audio_opts) as ydl:
ydl.download([url])
if not os.path.exists(audio_path):
raise Exception("TikTok audio file not created")
return video_path, audio_path
# ---------------- Instagram: Видео в 1080p (автоматически) --------------
def download_instagram_video(
url: str,
out_path: str,
cookies: str | None,
cancel_event: threading.Event,
progress_cb,
):
"""Instagram Reels: только видео в 1080p"""
ydl_opts = {
"format": f"bestvideo[height<={DEFAULT_INSTAGRAM_VIDEO_QUALITY}]+bestaudio/best",
"outtmpl": out_path.replace('.mp4', ''),
"merge_output_format": "mp4",
"cookiefile": cookies,
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
"quiet": True,
"no_warnings": True,
"extractor_args": {"instagram": {"skip_impersonation": True}},
"concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
# ---------------- AUDIO (для YouTube) --------------
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,
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
"postprocessors": [
{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
"preferredquality": "192",
},
],
"quiet": True,
"no_warnings": True,
"extractor_args": {
"tiktok": {"skip_impersonation": True},
"instagram": {"skip_impersonation": True},
},
"concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
# ---------------- ORIGINAL QUALITY (YouTube) --------------
def download_original_quality(
url: str,
out_path: str,
cookies: str | None,
cancel_event: threading.Event,
progress_cb,
):
"""Скачивает видео в оригинальном качестве (YouTube)"""
ydl_opts = {
"format": "best",
"outtmpl": out_path,
"merge_output_format": "mp4",
"cookiefile": cookies,
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
"quiet": True,
"no_warnings": True,
"extractor_args": {
"tiktok": {"skip_impersonation": True},
"instagram": {"skip_impersonation": True},
},
"format_sort": ["quality", "res", "codec", "size"],
"concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
# ---------------- 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"),
"merge_output_format": "mp4",
"cookiefile": cookies,
"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', [])
total_videos = len(entries)
logger.info(f"Starting playlist download with {total_videos} videos")
for i, entry in enumerate(entries, 1):
if cancel_event.is_set():
raise DownloadCancelled("Cancelled by user")
if not entry.get('url'):
logger.warning(f"Entry {i} has no URL, skipping")
continue
video_url = entry['url']
video_title = entry.get('title', f'Video {i}')
logger.info(f"Downloading video {i}/{total_videos}: {video_title}")
try:
info = ydl.extract_info(video_url, download=True)
if not info:
logger.error(f"Failed to extract info for video {i}")
continue
filename = ydl.prepare_filename(info)
if os.path.exists(filename):
downloaded_files.append(filename)
else:
base_name = filename.rsplit('.', 1)[0]
for ext in ['.mp4', '.mkv', '.webm', '.flv']:
alt_path = base_name + ext
if os.path.exists(alt_path):
downloaded_files.append(alt_path)
break
else:
logger.error(f"File not found for video {i}")
except Exception as e:
logger.error(f"Error downloading video {i} ({video_title}): {e}")
continue
logger.info(f"Playlist download complete. Downloaded {len(downloaded_files)} files")
return downloaded_files
except Exception as e:
logger.error(f"Error downloading playlist: {e}")
raise
# ---------------- METADATA (осталось без изменений) --------------
def add_metadata_to_audio(
input_path: str,
output_path: str,
metadata: dict,
):
if metadata is None:
metadata = {}
if not input_path.lower().endswith('.mp3'):
shutil.copy2(input_path, output_path)
return
metadata_args = []
if metadata.get('title'):
metadata_args.extend(['-metadata', f'title={metadata["title"][:100]}'])
if metadata.get('artist'):
metadata_args.extend(['-metadata', f'artist={metadata["artist"][:100]}'])
if metadata.get('album'):
metadata_args.extend(['-metadata', f'album={metadata["album"][:100]}'])
cmd = [
'ffmpeg',
'-i', input_path,
'-c', 'copy',
'-id3v2_version', '3',
'-loglevel', 'error',
'-y',
*metadata_args,
output_path
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30
)
if result.returncode != 0:
shutil.copy2(input_path, output_path)