update to 2.0
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -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,7 +60,130 @@ def download_video(
|
||||
ydl.download([url])
|
||||
|
||||
|
||||
# ---------------- ORIGINAL QUALITY ----------------
|
||||
# ---------------- 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,
|
||||
@@ -64,7 +192,7 @@ def download_original_quality(
|
||||
cancel_event: threading.Event,
|
||||
progress_cb,
|
||||
):
|
||||
"""Скачивает видео в оригинальном качестве"""
|
||||
"""Скачивает видео в оригинальном качестве (YouTube)"""
|
||||
ydl_opts = {
|
||||
"format": "best",
|
||||
"outtmpl": out_path,
|
||||
@@ -86,97 +214,7 @@ def download_original_quality(
|
||||
ydl.download([url])
|
||||
|
||||
|
||||
# ---------------- TIKTOK MUSIC ONLY ----------------
|
||||
|
||||
def download_tiktok_music(
|
||||
url: str,
|
||||
out_path: str,
|
||||
cookies: str | None,
|
||||
cancel_event: threading.Event,
|
||||
progress_cb,
|
||||
):
|
||||
"""Скачивает только звук из TikTok"""
|
||||
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},
|
||||
},
|
||||
"concurrent_fragment_downloads": 4,
|
||||
"http_chunk_size": 10485760,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
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 ----------------
|
||||
# ---------------- PLAYLIST (осталось без изменений) --------------
|
||||
|
||||
def download_playlist_videos(
|
||||
playlist_info: dict,
|
||||
@@ -185,7 +223,6 @@ def download_playlist_videos(
|
||||
cancel_event: threading.Event,
|
||||
progress_cb,
|
||||
) -> List[str]:
|
||||
"""Скачивает все видео из плейлиста"""
|
||||
downloaded_files = []
|
||||
|
||||
ydl_opts = {
|
||||
@@ -224,28 +261,22 @@ def download_playlist_videos(
|
||||
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}")
|
||||
@@ -262,36 +293,45 @@ def download_playlist_videos(
|
||||
raise
|
||||
|
||||
|
||||
# ---------------- STANDARD AUDIO ----------------
|
||||
# ---------------- METADATA (осталось без изменений) --------------
|
||||
|
||||
def download_audio(
|
||||
url: str,
|
||||
out_path: str,
|
||||
cookies: str | None,
|
||||
cancel_event: threading.Event,
|
||||
progress_cb,
|
||||
def add_metadata_to_audio(
|
||||
input_path: str,
|
||||
output_path: str,
|
||||
metadata: dict,
|
||||
):
|
||||
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,
|
||||
}
|
||||
if metadata is None:
|
||||
metadata = {}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([url])
|
||||
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)
|
||||
@@ -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 = [
|
||||
[
|
||||
@@ -46,44 +48,3 @@ def playlist_keyboard(confirm=False):
|
||||
]
|
||||
]
|
||||
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)
|
||||
866
bot/main.py
866
bot/main.py
File diff suppressed because it is too large
Load Diff
11
wireguard/wg_confs/wg0.conf
Normal file
11
wireguard/wg_confs/wg0.conf
Normal 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
|
||||
Reference in New Issue
Block a user