update to 2.0

This commit is contained in:
2026-02-18 14:05:36 +07:00
parent 7a2e73939f
commit c7ef7cdd57
6 changed files with 507 additions and 950 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -1,3 +1,4 @@
# config.py
import os
from dotenv import load_dotenv
@@ -19,8 +20,14 @@ COOKIES_FILE = os.getenv("COOKIES_FILE")
MAX_DURATION_SECONDS = int(os.getenv("MAX_DURATION_SECONDS", 1800))
RATE_LIMIT_SECONDS = int(os.getenv("RATE_LIMIT_SECONDS", 20))
MAX_FILE_SIZE_MB = 2000
CACHE_MAX_AGE_DAYS = 7 # Удалять файлы старше 7 дней
CACHE_MAX_SIZE_MB = 4096 # Максимальный размер кэша 1 ГБ (0 = без ограничения)
CACHE_MAX_AGE_DAYS = 7
CACHE_MAX_SIZE_MB = 4096
# Авто-качества для TikTok и Instagram (без выбора)
DEFAULT_TIKTOK_VIDEO_QUALITY = "1080"
DEFAULT_TIKTOK_AUDIO_QUALITY = "192" # кбит/с
DEFAULT_INSTAGRAM_VIDEO_QUALITY = "1080"
# Private mode
ALLOWED_USERS = set(
int(uid.strip())
@@ -30,4 +37,4 @@ ALLOWED_USERS = set(
# Ensure directories exist
os.makedirs(CACHE_DIR, exist_ok=True)
os.makedirs(TMP_DIR, exist_ok=True)
os.makedirs(TMP_DIR, exist_ok=True)

View File

@@ -1,12 +1,17 @@
# downloader.py
import yt_dlp
import threading
import os
import logging
import subprocess
import json
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__)
@@ -25,7 +30,7 @@ def _progress_hook(cancel_event, progress_cb):
return hook
# ---------------- STANDARD VIDEO ----------------
# ---------------- STANDARD VIDEO (YouTube/VK) --------------
def download_video(
url: str,
@@ -55,64 +60,86 @@ def download_video(
ydl.download([url])
# ---------------- ORIGINAL QUALITY ----------------
# ---------------- TikTok: Видео + Аудио (автоматически) --------------
def download_original_quality(
def download_tiktok_video_and_audio(
url: str,
out_path: str,
video_path: str,
audio_path: str,
cookies: str | None,
cancel_event: threading.Event,
progress_cb,
):
"""Скачивает видео в оригинальном качестве"""
ydl_opts = {
"format": "best",
"outtmpl": out_path,
"""
Скачивает 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},
"instagram": {"skip_impersonation": True},
},
"format_sort": ["quality", "res", "codec", "size"],
"extractor_args": {"tiktok": {"skip_impersonation": True}},
"concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
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")
# ---------------- TIKTOK MUSIC ONLY ----------------
def download_tiktok_music(
url: str,
out_path: str,
cookies: str | None,
cancel_event: threading.Event,
progress_cb,
):
"""Скачивает только звук из TikTok"""
ydl_opts = {
# 2. Извлекаем аудио в MP3
audio_opts = {
"format": "bestaudio/best",
"outtmpl": out_path.replace('.mp3', ''),
"outtmpl": audio_path.replace('.mp3', ''),
"cookiefile": cookies,
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
"postprocessors": [
{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
"preferredquality": "192",
},
],
"postprocessors": [{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
"preferredquality": DEFAULT_TIKTOK_AUDIO_QUALITY,
}],
"quiet": True,
"no_warnings": True,
"extractor_args": {
"tiktok": {"skip_impersonation": 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,
}
@@ -121,148 +148,7 @@ def download_tiktok_music(
ydl.download([url])
# ---------------- ADD METADATA TO AUDIO ----------------
def add_metadata_to_audio(
input_path: str,
output_path: str,
metadata: dict,
):
"""Добавляет метаданные к аудио файлу"""
try:
# Проверяем metadata
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)
except Exception as e:
logger.error(f"Metadata error: {e}")
shutil.copy2(input_path, output_path)
# ---------------- PLAYLIST DOWNLOAD ----------------
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)
logger.info(f"Successfully downloaded: {os.path.basename(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)
logger.info(f"Found file with extension {ext}: {os.path.basename(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
# ---------------- STANDARD AUDIO ----------------
# ---------------- AUDIO (для YouTube) --------------
def download_audio(
url: str,
@@ -294,4 +180,158 @@ def download_audio(
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
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)

View File

@@ -1,14 +1,16 @@
# keyboards.py
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
def quality_keyboard():
"""Клавиатура выбора качества"""
def youtube_quality_keyboard():
"""YouTube/VK: выбор качества"""
keyboard = [
[
InlineKeyboardButton(text="📹 480p", callback_data="q:480"),
InlineKeyboardButton(text="📹 720p", callback_data="q:720"),
],
[
InlineKeyboardButton(text="🎬 1080p", callback_data="q:1080"),
InlineKeyboardButton(text="🎥 1440p", callback_data="q:1440"),
],
[
InlineKeyboardButton(text="🎧 Аудио", callback_data="audio"),
@@ -19,7 +21,7 @@ def quality_keyboard():
def cancel_keyboard():
"""Клавиатура с кнопкой отмены"""
"""Клавиатура с кнопкой отмены (для плейлистов)"""
keyboard = [
[InlineKeyboardButton(text="⛔ Отменить загрузку", callback_data="cancel")]
]
@@ -27,7 +29,7 @@ def cancel_keyboard():
def playlist_keyboard(confirm=False):
"""Клавиатура для плейлистов"""
"""Клавиатура для плейлистов YouTube"""
if confirm:
keyboard = [
[
@@ -45,45 +47,4 @@ def playlist_keyboard(confirm=False):
InlineKeyboardButton(text="❌ Отмена", callback_data="cancel"),
]
]
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def platform_keyboard(platform):
"""Клавиатура для Instagram"""
keyboard = [
[
InlineKeyboardButton(text=f"🎬 Оригинальное качество", callback_data="original_quality"),
],
[
InlineKeyboardButton(text="📹 480p", callback_data="q:480"),
InlineKeyboardButton(text="📹 720p", callback_data="q:720"),
InlineKeyboardButton(text="🎬 1080p", callback_data="q:1080"),
],
[
InlineKeyboardButton(text="🎧 Аудио", callback_data="audio"),
InlineKeyboardButton(text="❌ Отмена", callback_data="cancel"),
]
]
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def tiktok_keyboard():
"""Специальная клавиатура для TikTok"""
keyboard = [
[
InlineKeyboardButton(text="🎬 Оригинальное качество", callback_data="original_quality"),
],
[
InlineKeyboardButton(text="📹 480p", callback_data="q:480"),
InlineKeyboardButton(text="📹 720p", callback_data="q:720"),
InlineKeyboardButton(text="🎬 1080p", callback_data="q:1080"),
],
[
InlineKeyboardButton(text="🎧 Аудио из видео", callback_data="audio"),
InlineKeyboardButton(text="🎵 Только звук (TikTok)", callback_data="tiktok_music"),
],
[
InlineKeyboardButton(text="❌ Отмена", callback_data="cancel"),
]
]
return InlineKeyboardMarkup(inline_keyboard=keyboard)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
[Interface]
Address = 10.7.0.13/24
DNS = 1.0.0.1, 1.1.1.1, 1.0.0.1, 1.1.1.1
PrivateKey = 8GTpMYtpOhWDp6AKXxukwFRsiwvERf2UVCjCrdB+M10=
[Peer]
PublicKey = ekvrE4SMgydQhkWdWgJazlrILSEAlRbwCI4KcXUQv3A=
PresharedKey = eaCOqeiYdfso4v7/ucrvFzB1MtDnXy1yTiujL08jJIE=
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 109.107.170.183:51821
PersistentKeepalive = 25