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 import os
from dotenv import load_dotenv 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)) MAX_DURATION_SECONDS = int(os.getenv("MAX_DURATION_SECONDS", 1800))
RATE_LIMIT_SECONDS = int(os.getenv("RATE_LIMIT_SECONDS", 20)) RATE_LIMIT_SECONDS = int(os.getenv("RATE_LIMIT_SECONDS", 20))
MAX_FILE_SIZE_MB = 2000 MAX_FILE_SIZE_MB = 2000
CACHE_MAX_AGE_DAYS = 7 # Удалять файлы старше 7 дней CACHE_MAX_AGE_DAYS = 7
CACHE_MAX_SIZE_MB = 4096 # Максимальный размер кэша 1 ГБ (0 = без ограничения) CACHE_MAX_SIZE_MB = 4096
# Авто-качества для TikTok и Instagram (без выбора)
DEFAULT_TIKTOK_VIDEO_QUALITY = "1080"
DEFAULT_TIKTOK_AUDIO_QUALITY = "192" # кбит/с
DEFAULT_INSTAGRAM_VIDEO_QUALITY = "1080"
# Private mode # Private mode
ALLOWED_USERS = set( ALLOWED_USERS = set(
int(uid.strip()) int(uid.strip())
@@ -30,4 +37,4 @@ ALLOWED_USERS = set(
# Ensure directories exist # Ensure directories exist
os.makedirs(CACHE_DIR, exist_ok=True) 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 yt_dlp
import threading import threading
import os import os
import logging import logging
import subprocess
import json
import shutil import shutil
from typing import List, Optional from typing import List, Optional
from config import (
DEFAULT_TIKTOK_VIDEO_QUALITY,
DEFAULT_TIKTOK_AUDIO_QUALITY,
DEFAULT_INSTAGRAM_VIDEO_QUALITY,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -25,7 +30,7 @@ def _progress_hook(cancel_event, progress_cb):
return hook return hook
# ---------------- STANDARD VIDEO ---------------- # ---------------- STANDARD VIDEO (YouTube/VK) --------------
def download_video( def download_video(
url: str, url: str,
@@ -55,64 +60,86 @@ def download_video(
ydl.download([url]) ydl.download([url])
# ---------------- ORIGINAL QUALITY ---------------- # ---------------- TikTok: Видео + Аудио (автоматически) --------------
def download_original_quality( def download_tiktok_video_and_audio(
url: str, url: str,
out_path: str, video_path: str,
audio_path: str,
cookies: str | None, cookies: str | None,
cancel_event: threading.Event, cancel_event: threading.Event,
progress_cb, progress_cb,
): ):
"""Скачивает видео в оригинальном качестве""" """
ydl_opts = { Скачивает TikTok: видео (1080p) + аудио (MP3 192kbps).
"format": "best", Возвращает (video_path, audio_path) или None.
"outtmpl": out_path, """
# 1. Скачиваем видео в 1080p
video_opts = {
"format": f"bestvideo[height<={DEFAULT_TIKTOK_VIDEO_QUALITY}]+bestaudio/best",
"outtmpl": video_path.replace('.mp4', ''),
"merge_output_format": "mp4", "merge_output_format": "mp4",
"cookiefile": cookies, "cookiefile": cookies,
"progress_hooks": [_progress_hook(cancel_event, progress_cb)], "progress_hooks": [_progress_hook(cancel_event, progress_cb)],
"quiet": True, "quiet": True,
"no_warnings": True, "no_warnings": True,
"extractor_args": { "extractor_args": {"tiktok": {"skip_impersonation": True}},
"tiktok": {"skip_impersonation": True},
"instagram": {"skip_impersonation": True},
},
"format_sort": ["quality", "res", "codec", "size"],
"concurrent_fragment_downloads": 4, "concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760, "http_chunk_size": 10485760,
} }
with yt_dlp.YoutubeDL(ydl_opts) as ydl: with yt_dlp.YoutubeDL(video_opts) as ydl:
ydl.download([url]) ydl.download([url])
# Проверяем, что видео создано
if not os.path.exists(video_path):
raise Exception("TikTok video file not created")
# ---------------- TIKTOK MUSIC ONLY ---------------- # 2. Извлекаем аудио в MP3
audio_opts = {
def download_tiktok_music(
url: str,
out_path: str,
cookies: str | None,
cancel_event: threading.Event,
progress_cb,
):
"""Скачивает только звук из TikTok"""
ydl_opts = {
"format": "bestaudio/best", "format": "bestaudio/best",
"outtmpl": out_path.replace('.mp3', ''), "outtmpl": audio_path.replace('.mp3', ''),
"cookiefile": cookies, "cookiefile": cookies,
"progress_hooks": [_progress_hook(cancel_event, progress_cb)], "postprocessors": [{
"postprocessors": [ "key": "FFmpegExtractAudio",
{ "preferredcodec": "mp3",
"key": "FFmpegExtractAudio", "preferredquality": DEFAULT_TIKTOK_AUDIO_QUALITY,
"preferredcodec": "mp3", }],
"preferredquality": "192",
},
],
"quiet": True, "quiet": True,
"no_warnings": True, "no_warnings": True,
"extractor_args": { "extractor_args": {"tiktok": {"skip_impersonation": True}},
"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, "concurrent_fragment_downloads": 4,
"http_chunk_size": 10485760, "http_chunk_size": 10485760,
} }
@@ -121,148 +148,7 @@ def download_tiktok_music(
ydl.download([url]) ydl.download([url])
# ---------------- ADD METADATA TO AUDIO ---------------- # ---------------- AUDIO (для YouTube) --------------
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 ----------------
def download_audio( def download_audio(
url: str, url: str,
@@ -294,4 +180,158 @@ def download_audio(
} }
with yt_dlp.YoutubeDL(ydl_opts) as ydl: 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 from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
def quality_keyboard(): def youtube_quality_keyboard():
"""Клавиатура выбора качества""" """YouTube/VK: выбор качества"""
keyboard = [ keyboard = [
[ [
InlineKeyboardButton(text="📹 480p", callback_data="q:480"), InlineKeyboardButton(text="📹 480p", callback_data="q:480"),
InlineKeyboardButton(text="📹 720p", callback_data="q:720"), InlineKeyboardButton(text="📹 720p", callback_data="q:720"),
],
[
InlineKeyboardButton(text="🎬 1080p", callback_data="q:1080"), InlineKeyboardButton(text="🎬 1080p", callback_data="q:1080"),
InlineKeyboardButton(text="🎥 1440p", callback_data="q:1440"),
], ],
[ [
InlineKeyboardButton(text="🎧 Аудио", callback_data="audio"), InlineKeyboardButton(text="🎧 Аудио", callback_data="audio"),
@@ -19,7 +21,7 @@ def quality_keyboard():
def cancel_keyboard(): def cancel_keyboard():
"""Клавиатура с кнопкой отмены""" """Клавиатура с кнопкой отмены (для плейлистов)"""
keyboard = [ keyboard = [
[InlineKeyboardButton(text="⛔ Отменить загрузку", callback_data="cancel")] [InlineKeyboardButton(text="⛔ Отменить загрузку", callback_data="cancel")]
] ]
@@ -27,7 +29,7 @@ def cancel_keyboard():
def playlist_keyboard(confirm=False): def playlist_keyboard(confirm=False):
"""Клавиатура для плейлистов""" """Клавиатура для плейлистов YouTube"""
if confirm: if confirm:
keyboard = [ keyboard = [
[ [
@@ -45,45 +47,4 @@ def playlist_keyboard(confirm=False):
InlineKeyboardButton(text="❌ Отмена", callback_data="cancel"), 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) 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