update_0.2 #1
+192
-88
@@ -3,8 +3,8 @@ import yt_dlp
|
|||||||
import threading
|
import threading
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import shutil
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
@@ -26,7 +26,134 @@ def _progress_hook(cancel_event, progress_cb):
|
|||||||
progress_cb(d)
|
progress_cb(d)
|
||||||
return hook
|
return hook
|
||||||
|
|
||||||
# ---------------- TIKTOK: Видео + Аудио (автоматически) --------------
|
def optimize_for_telegram(input_path: str, output_path: str) -> None:
|
||||||
|
"""
|
||||||
|
Конвертирует видео в Telegram-safe формат:
|
||||||
|
- mp4 + h264 + aac
|
||||||
|
- четные размеры
|
||||||
|
- crf: 23 (или 28 >50MB)
|
||||||
|
- faststart
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
):
|
||||||
|
# 1. Скачиваем в любой формат
|
||||||
|
ydl_opts = {
|
||||||
|
"format": f"best[height<={quality}]/best",
|
||||||
|
"outtmpl": out_path, # ← без .mp4!
|
||||||
|
"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])
|
||||||
|
|
||||||
|
# 2. Ищем реальный файл (без расширения, .mp4, .mov, .webm)
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# 3. Конвертируем в Telegram-safe mp4
|
||||||
|
optimized_path = base + "_telegram.mp4"
|
||||||
|
await asyncio.to_thread(optimize_for_telegram, actual_video, optimized_path)
|
||||||
|
|
||||||
|
# 4. Удаляем оригинальный файл и ставим конвертированный
|
||||||
|
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', ''), # ← без .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])
|
||||||
|
|
||||||
|
# Ищем файл (может быть .m4a, .opus, .weba)
|
||||||
|
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"
|
||||||
|
subprocess.run([
|
||||||
|
'ffmpeg', '-i', actual_audio, '-c:a', 'aac', '-b:a', '192k',
|
||||||
|
'-loglevel', 'error', '-y', 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(
|
def download_tiktok_video_and_audio(
|
||||||
url: str,
|
url: str,
|
||||||
video_path: str,
|
video_path: str,
|
||||||
@@ -35,15 +162,14 @@ def download_tiktok_video_and_audio(
|
|||||||
cancel_event: threading.Event,
|
cancel_event: threading.Event,
|
||||||
progress_cb,
|
progress_cb,
|
||||||
):
|
):
|
||||||
# 1. Скачиваем видео (1080p) — УБРАН skip_impersonation
|
# 1. Скачиваем видео
|
||||||
video_opts = {
|
video_opts = {
|
||||||
"format": f"bestvideo[height<={DEFAULT_TIKTOK_VIDEO_QUALITY}]+bestaudio/best",
|
"format": f"bestvideo[height<={DEFAULT_TIKTOK_VIDEO_QUALITY}]+bestaudio/best",
|
||||||
"outtmpl": video_path.replace('.mp4', ''),
|
"outtmpl": video_path, # ← без .mp4!
|
||||||
"merge_output_format": "mp4",
|
|
||||||
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
||||||
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
|
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
|
||||||
"quiet": True,
|
"quiet": True,
|
||||||
"no_warnings": False, # ← ВКЛЮЧИЛ предупреждения для отладки
|
"no_warnings": False,
|
||||||
"concurrent_fragment_downloads": 4,
|
"concurrent_fragment_downloads": 4,
|
||||||
"http_chunk_size": 10485760,
|
"http_chunk_size": 10485760,
|
||||||
}
|
}
|
||||||
@@ -51,10 +177,24 @@ def download_tiktok_video_and_audio(
|
|||||||
with yt_dlp.YoutubeDL(video_opts) as ydl:
|
with yt_dlp.YoutubeDL(video_opts) as ydl:
|
||||||
ydl.download([url])
|
ydl.download([url])
|
||||||
|
|
||||||
if not os.path.exists(video_path):
|
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")
|
raise Exception("TikTok video file not created")
|
||||||
|
|
||||||
# 2. Извлекаем аудио (MP3 192kbps) — УБРАН skip_impersonation
|
# Конвертируем в Telegram-safe
|
||||||
|
optimized_video = base_video + "_telegram.mp4"
|
||||||
|
await asyncio.to_thread(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)
|
||||||
|
|
||||||
|
# 2. Аудио
|
||||||
audio_opts = {
|
audio_opts = {
|
||||||
"format": "bestaudio/best",
|
"format": "bestaudio/best",
|
||||||
"outtmpl": audio_path.replace('.mp3', ''),
|
"outtmpl": audio_path.replace('.mp3', ''),
|
||||||
@@ -65,7 +205,7 @@ def download_tiktok_video_and_audio(
|
|||||||
"preferredquality": DEFAULT_TIKTOK_AUDIO_QUALITY,
|
"preferredquality": DEFAULT_TIKTOK_AUDIO_QUALITY,
|
||||||
}],
|
}],
|
||||||
"quiet": True,
|
"quiet": True,
|
||||||
"no_warnings": False, # ← ВКЛЮЧИЛ для отладки
|
"no_warnings": False,
|
||||||
"concurrent_fragment_downloads": 4,
|
"concurrent_fragment_downloads": 4,
|
||||||
"http_chunk_size": 10485760,
|
"http_chunk_size": 10485760,
|
||||||
}
|
}
|
||||||
@@ -73,10 +213,27 @@ def download_tiktok_video_and_audio(
|
|||||||
with yt_dlp.YoutubeDL(audio_opts) as ydl:
|
with yt_dlp.YoutubeDL(audio_opts) as ydl:
|
||||||
ydl.download([url])
|
ydl.download([url])
|
||||||
|
|
||||||
if not os.path.exists(audio_path):
|
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")
|
raise Exception("TikTok audio file not created")
|
||||||
|
|
||||||
# ---------------- INSTAGRAM: Видео в 1080p (автоматически) --------------
|
if actual_audio != audio_path:
|
||||||
|
optimized_audio = base_audio + "_telegram.mp3"
|
||||||
|
subprocess.run([
|
||||||
|
'ffmpeg', '-i', actual_audio, '-c:a', 'aac', '-b:a', '192k',
|
||||||
|
'-loglevel', 'error', '-y', 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(
|
def download_instagram_video(
|
||||||
url: str,
|
url: str,
|
||||||
out_path: str,
|
out_path: str,
|
||||||
@@ -84,68 +241,37 @@ def download_instagram_video(
|
|||||||
cancel_event: threading.Event,
|
cancel_event: threading.Event,
|
||||||
progress_cb,
|
progress_cb,
|
||||||
):
|
):
|
||||||
# УБРАН skip_impersonation
|
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
"format": f"bestvideo[height<={DEFAULT_INSTAGRAM_VIDEO_QUALITY}]+bestaudio/best",
|
"format": f"bestvideo[height<={DEFAULT_INSTAGRAM_VIDEO_QUALITY}]+bestaudio/best",
|
||||||
"outtmpl": out_path.replace('.mp4', ''),
|
|
||||||
"merge_output_format": "mp4",
|
|
||||||
"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])
|
|
||||||
|
|
||||||
# ---------------- 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])
|
|
||||||
|
|
||||||
# ---------------- 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,
|
"outtmpl": out_path,
|
||||||
"merge_output_format": "mp4",
|
|
||||||
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
||||||
"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": False,
|
||||||
"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(ydl_opts) as ydl:
|
||||||
ydl.download([url])
|
ydl.download([url])
|
||||||
|
|
||||||
# ---------------- PLAYLIST (осталось без изменений) --------------
|
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"
|
||||||
|
await asyncio.to_thread(optimize_for_telegram, actual, optimized)
|
||||||
|
if os.path.exists(out_path): os.remove(out_path)
|
||||||
|
os.rename(optimized, out_path)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- PLAYLIST ------
|
||||||
def download_playlist_videos(
|
def download_playlist_videos(
|
||||||
playlist_info: dict,
|
playlist_info: dict,
|
||||||
output_dir: str,
|
output_dir: str,
|
||||||
@@ -157,7 +283,6 @@ def download_playlist_videos(
|
|||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
"format": "best[height<=1080]/best",
|
"format": "best[height<=1080]/best",
|
||||||
"outtmpl": os.path.join(output_dir, "%(title)s [%(id)s].%(ext)s"),
|
"outtmpl": os.path.join(output_dir, "%(title)s [%(id)s].%(ext)s"),
|
||||||
"merge_output_format": "mp4",
|
|
||||||
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
||||||
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
|
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
|
||||||
"quiet": True,
|
"quiet": True,
|
||||||
@@ -168,45 +293,24 @@ def download_playlist_videos(
|
|||||||
"concurrent_fragment_downloads": 4,
|
"concurrent_fragment_downloads": 4,
|
||||||
"http_chunk_size": 10485760,
|
"http_chunk_size": 10485760,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
entries = playlist_info.get('entries', [])
|
entries = playlist_info.get('entries', [])
|
||||||
total = len(entries)
|
|
||||||
logger.info(f"Downloading playlist: {total} videos")
|
|
||||||
for i, entry in enumerate(entries, 1):
|
for i, entry in enumerate(entries, 1):
|
||||||
if cancel_event.is_set():
|
if cancel_event.is_set():
|
||||||
raise DownloadCancelled("Cancelled by user")
|
raise DownloadCancelled("Cancelled by user")
|
||||||
if not entry.get('url'):
|
if not entry.get('url'):
|
||||||
logger.warning(f"Entry {i} has no URL")
|
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
info = ydl.extract_info(entry['url'], download=True)
|
info = ydl.extract_info(entry['url'], download=True)
|
||||||
if info and os.path.exists(ydl.prepare_filename(info)):
|
if info:
|
||||||
downloaded_files.append(ydl.prepare_filename(info))
|
filename = ydl.prepare_filename(info)
|
||||||
|
if os.path.exists(filename):
|
||||||
|
downloaded_files.append(filename)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error downloading video {i}: {e}")
|
logger.error(f"Error downloading video {i}: {e}")
|
||||||
continue
|
|
||||||
return downloaded_files
|
return downloaded_files
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Playlist download error: {e}")
|
logger.error(f"Playlist download error: {e}")
|
||||||
raise
|
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 = []
|
|
||||||
for key in ['title', 'artist', 'album']:
|
|
||||||
if metadata.get(key):
|
|
||||||
metadata_args.extend(['-metadata', f'{key}={str(metadata[key])[: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)
|
|
||||||
+12
-35
@@ -1,4 +1,4 @@
|
|||||||
# main.py — ИСПРАВЛЕНО: добавлен import cache_key
|
# main.py
|
||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
import threading
|
import threading
|
||||||
@@ -17,8 +17,6 @@ from config import (
|
|||||||
CACHE_DIR,
|
CACHE_DIR,
|
||||||
TMP_DIR,
|
TMP_DIR,
|
||||||
COOKIES_FILE,
|
COOKIES_FILE,
|
||||||
CACHE_MAX_AGE_DAYS,
|
|
||||||
CACHE_MAX_SIZE_MB,
|
|
||||||
)
|
)
|
||||||
from keyboards import youtube_quality_keyboard, cancel_keyboard, playlist_keyboard
|
from keyboards import youtube_quality_keyboard, cancel_keyboard, playlist_keyboard
|
||||||
from downloader import (
|
from downloader import (
|
||||||
@@ -27,18 +25,17 @@ from downloader import (
|
|||||||
download_video,
|
download_video,
|
||||||
download_audio,
|
download_audio,
|
||||||
download_playlist_videos,
|
download_playlist_videos,
|
||||||
|
optimize_for_telegram,
|
||||||
DownloadCancelled,
|
DownloadCancelled,
|
||||||
)
|
)
|
||||||
from middleware import PrivateMiddleware
|
from middleware import PrivateMiddleware
|
||||||
from rate_limit import check_rate_limit
|
|
||||||
from info import extract_info, is_playlist, get_platform_info
|
from info import extract_info, is_playlist, get_platform_info
|
||||||
from cache import cache_key, cache_path # ← ИСПРАВЛЕНО: добавлен cache_key
|
from cache import cache_key, cache_path
|
||||||
from cleanup import cleanup_tmp
|
from cleanup import cleanup_tmp
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Инициализация
|
|
||||||
if LOCAL_API_URL:
|
if LOCAL_API_URL:
|
||||||
api_server = TelegramAPIServer.from_base(LOCAL_API_URL)
|
api_server = TelegramAPIServer.from_base(LOCAL_API_URL)
|
||||||
session = AiohttpSession(api=api_server)
|
session = AiohttpSession(api=api_server)
|
||||||
@@ -87,8 +84,7 @@ def make_progress_cb(loop, message):
|
|||||||
asyncio.run_coroutine_threadsafe(update(d), loop)
|
asyncio.run_coroutine_threadsafe(update(d), loop)
|
||||||
return cb
|
return cb
|
||||||
|
|
||||||
# ---------------------- AUTO DOWNLOADS (TikTok/Instagram) ------------------
|
# ---------------- TikTok + Instagram (автозагрузка) ----------------
|
||||||
|
|
||||||
async def process_tiktok_auto(message: Message, user_id: int, url: str):
|
async def process_tiktok_auto(message: Message, user_id: int, url: str):
|
||||||
status = await message.answer("🎵 <b>Загружаю TikTok (видео + аудио)...</b>", parse_mode="HTML")
|
status = await message.answer("🎵 <b>Загружаю TikTok (видео + аудио)...</b>", parse_mode="HTML")
|
||||||
|
|
||||||
@@ -103,7 +99,6 @@ async def process_tiktok_auto(message: Message, user_id: int, url: str):
|
|||||||
tmp_audio = cache_path(TMP_DIR, key_audio, "mp3")
|
tmp_audio = cache_path(TMP_DIR, key_audio, "mp3")
|
||||||
os.makedirs(os.path.dirname(tmp_audio), exist_ok=True)
|
os.makedirs(os.path.dirname(tmp_audio), exist_ok=True)
|
||||||
|
|
||||||
# Проверка кэша
|
|
||||||
if os.path.exists(video_cache) and os.path.exists(audio_cache):
|
if os.path.exists(video_cache) and os.path.exists(audio_cache):
|
||||||
await status.edit_text("📤 <b>Отправляю из кэша...</b>", parse_mode="HTML")
|
await status.edit_text("📤 <b>Отправляю из кэша...</b>", parse_mode="HTML")
|
||||||
try:
|
try:
|
||||||
@@ -118,7 +113,6 @@ async def process_tiktok_auto(message: Message, user_id: int, url: str):
|
|||||||
cleanup_tmp(TMP_DIR)
|
cleanup_tmp(TMP_DIR)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Загрузка
|
|
||||||
cancel_event = threading.Event()
|
cancel_event = threading.Event()
|
||||||
ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event}
|
ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event}
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
@@ -134,19 +128,16 @@ async def process_tiktok_auto(message: Message, user_id: int, url: str):
|
|||||||
cancel_event,
|
cancel_event,
|
||||||
progress_cb,
|
progress_cb,
|
||||||
)
|
)
|
||||||
|
|
||||||
if cancel_event.is_set():
|
if cancel_event.is_set():
|
||||||
await status.edit_text("⛔ Загрузка отменена")
|
await status.edit_text("⛔ Загрузка отменена")
|
||||||
for p in [tmp_video, tmp_audio]:
|
for p in [tmp_video, tmp_audio]:
|
||||||
if os.path.exists(p): os.remove(p)
|
if os.path.exists(p): os.remove(p)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Перемещение в кэш
|
# Уже готовы Telegram-safe mp4/m3, просто перемещаем
|
||||||
for path in [video_cache, audio_cache]:
|
if os.path.exists(video_cache): os.remove(video_cache)
|
||||||
if os.path.exists(path): os.remove(path)
|
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(video_cache), exist_ok=True)
|
|
||||||
os.rename(tmp_video, video_cache)
|
os.rename(tmp_video, video_cache)
|
||||||
|
if os.path.exists(audio_cache): os.remove(audio_cache)
|
||||||
os.rename(tmp_audio, audio_cache)
|
os.rename(tmp_audio, audio_cache)
|
||||||
|
|
||||||
await status.edit_text("📤 <b>Отправляю видео + аудио...</b>", parse_mode="HTML")
|
await status.edit_text("📤 <b>Отправляю видео + аудио...</b>", parse_mode="HTML")
|
||||||
@@ -170,7 +161,6 @@ async def process_instagram_auto(message: Message, user_id: int, url: str):
|
|||||||
tmp_path = cache_path(TMP_DIR, key, "mp4")
|
tmp_path = cache_path(TMP_DIR, key, "mp4")
|
||||||
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
|
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
|
||||||
|
|
||||||
# Проверка кэша
|
|
||||||
if os.path.exists(final_cache):
|
if os.path.exists(final_cache):
|
||||||
await status.edit_text("📤 <b>Отправляю из кэша...</b>", parse_mode="HTML")
|
await status.edit_text("📤 <b>Отправляю из кэша...</b>", parse_mode="HTML")
|
||||||
try:
|
try:
|
||||||
@@ -198,15 +188,12 @@ async def process_instagram_auto(message: Message, user_id: int, url: str):
|
|||||||
cancel_event,
|
cancel_event,
|
||||||
progress_cb,
|
progress_cb,
|
||||||
)
|
)
|
||||||
|
|
||||||
if cancel_event.is_set():
|
if cancel_event.is_set():
|
||||||
await status.edit_text("⛔ Загрузка отменена")
|
await status.edit_text("⛔ Загрузка отменена")
|
||||||
if os.path.exists(tmp_path): os.remove(tmp_path)
|
if os.path.exists(tmp_path): os.remove(tmp_path)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Перемещение в кэш
|
|
||||||
if os.path.exists(final_cache): os.remove(final_cache)
|
if os.path.exists(final_cache): os.remove(final_cache)
|
||||||
os.makedirs(os.path.dirname(final_cache), exist_ok=True)
|
|
||||||
os.rename(tmp_path, final_cache)
|
os.rename(tmp_path, final_cache)
|
||||||
|
|
||||||
await status.edit_text("📤 <b>Отправляю видео...</b>", parse_mode="HTML")
|
await status.edit_text("📤 <b>Отправляю видео...</b>", parse_mode="HTML")
|
||||||
@@ -221,16 +208,15 @@ async def process_instagram_auto(message: Message, user_id: int, url: str):
|
|||||||
ACTIVE_DOWNLOADS.pop(user_id, None)
|
ACTIVE_DOWNLOADS.pop(user_id, None)
|
||||||
cleanup_tmp(TMP_DIR)
|
cleanup_tmp(TMP_DIR)
|
||||||
|
|
||||||
# ---------------------- HANDLERS ------------------
|
# ---------------- Handlers ----------------
|
||||||
|
|
||||||
@dp.message(F.text == "/start")
|
@dp.message(F.text == "/start")
|
||||||
async def start(message: Message):
|
async def start(message: Message):
|
||||||
await message.answer(
|
await message.answer(
|
||||||
"👋 <b>Привет!</b>\n\n"
|
"👋 <b>Привет!</b>\n\n"
|
||||||
"📥 Скачиваю видео и аудио по ссылке.\n\n"
|
"📥 Скачиваю видео и аудио по ссылке.\n\n"
|
||||||
"✨ <b>Новые возможности:</b>\n"
|
"✨ <b>Новые возможности:</b>\n"
|
||||||
"• 🎬 TikTok: автоматически видео + аудио\n"
|
"• 🎬 TikTok: автоматически видео + аудио (Telegram-safe)\n"
|
||||||
"• 📸 Instagram: автоматически видео в 1080p\n"
|
"• 📸 Instagram: автоматически видео в 1080p (Telegram-safe)\n"
|
||||||
"• 🎵 Аудио из любого видео\n"
|
"• 🎵 Аудио из любого видео\n"
|
||||||
"• 📁 Плейлисты YouTube\n"
|
"• 📁 Плейлисты YouTube\n"
|
||||||
"👉 Просто отправь ссылку.",
|
"👉 Просто отправь ссылку.",
|
||||||
@@ -245,17 +231,14 @@ async def handle_link(message: Message):
|
|||||||
|
|
||||||
platform_info = await asyncio.to_thread(get_platform_info, url)
|
platform_info = await asyncio.to_thread(get_platform_info, url)
|
||||||
|
|
||||||
# TikTok — автоматически
|
|
||||||
if platform_info == "tiktok":
|
if platform_info == "tiktok":
|
||||||
await process_tiktok_auto(message, user_id, url)
|
await process_tiktok_auto(message, user_id, url)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Instagram — автоматически
|
|
||||||
if platform_info == "instagram":
|
if platform_info == "instagram":
|
||||||
await process_instagram_auto(message, user_id, url)
|
await process_instagram_auto(message, user_id, url)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Плейлисты
|
|
||||||
if await asyncio.to_thread(is_playlist, url):
|
if await asyncio.to_thread(is_playlist, url):
|
||||||
await message.answer(
|
await message.answer(
|
||||||
"📁 <b>Обнаружен плейлист!</b>\n\n"
|
"📁 <b>Обнаружен плейлист!</b>\n\n"
|
||||||
@@ -265,7 +248,6 @@ async def handle_link(message: Message):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# YouTube/VK — выбор качества
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
"🔽 <b>Выбери качество:</b>",
|
"🔽 <b>Выбери качество:</b>",
|
||||||
reply_markup=youtube_quality_keyboard(),
|
reply_markup=youtube_quality_keyboard(),
|
||||||
@@ -291,7 +273,6 @@ async def handle_video(callback: CallbackQuery):
|
|||||||
tmp_path = cache_path(TMP_DIR, key, "mp4")
|
tmp_path = cache_path(TMP_DIR, key, "mp4")
|
||||||
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
|
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
|
||||||
|
|
||||||
# Проверка кэша
|
|
||||||
if os.path.exists(final_path):
|
if os.path.exists(final_path):
|
||||||
await status.edit_text("📤 <b>Отправляю файл из кэша…</b>", parse_mode="HTML")
|
await status.edit_text("📤 <b>Отправляю файл из кэша…</b>", parse_mode="HTML")
|
||||||
try:
|
try:
|
||||||
@@ -318,14 +299,12 @@ async def handle_video(callback: CallbackQuery):
|
|||||||
cancel_event,
|
cancel_event,
|
||||||
progress_cb,
|
progress_cb,
|
||||||
)
|
)
|
||||||
|
|
||||||
if cancel_event.is_set():
|
if cancel_event.is_set():
|
||||||
if os.path.exists(tmp_path): os.remove(tmp_path)
|
if os.path.exists(tmp_path): os.remove(tmp_path)
|
||||||
await status.edit_text("⛔ Загрузка отменена")
|
await status.edit_text("⛔ Загрузка отменена")
|
||||||
return
|
return
|
||||||
|
|
||||||
if os.path.exists(final_path): os.remove(final_path)
|
if os.path.exists(final_path): os.remove(final_path)
|
||||||
os.makedirs(os.path.dirname(final_path), exist_ok=True)
|
|
||||||
os.rename(tmp_path, final_path)
|
os.rename(tmp_path, final_path)
|
||||||
|
|
||||||
except DownloadCancelled:
|
except DownloadCancelled:
|
||||||
@@ -372,7 +351,6 @@ async def handle_audio(callback: CallbackQuery):
|
|||||||
tmp_path = cache_path(TMP_DIR, key, "mp3")
|
tmp_path = cache_path(TMP_DIR, key, "mp3")
|
||||||
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
|
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
|
||||||
|
|
||||||
# Проверка кэша
|
|
||||||
if os.path.exists(final_path):
|
if os.path.exists(final_path):
|
||||||
await status.edit_text("📤 <b>Отправляю аудио из кэша…</b>", parse_mode="HTML")
|
await status.edit_text("📤 <b>Отправляю аудио из кэша…</b>", parse_mode="HTML")
|
||||||
try:
|
try:
|
||||||
@@ -398,7 +376,6 @@ async def handle_audio(callback: CallbackQuery):
|
|||||||
cancel_event,
|
cancel_event,
|
||||||
progress_cb,
|
progress_cb,
|
||||||
)
|
)
|
||||||
|
|
||||||
if cancel_event.is_set():
|
if cancel_event.is_set():
|
||||||
await status.edit_text("⛔ Загрузка отменена")
|
await status.edit_text("⛔ Загрузка отменена")
|
||||||
if os.path.exists(tmp_path): os.remove(tmp_path)
|
if os.path.exists(tmp_path): os.remove(tmp_path)
|
||||||
@@ -410,6 +387,7 @@ async def handle_audio(callback: CallbackQuery):
|
|||||||
os.remove(tmp_path)
|
os.remove(tmp_path)
|
||||||
raise Exception("Создан пустой аудио файл")
|
raise Exception("Создан пустой аудио файл")
|
||||||
|
|
||||||
|
if os.path.exists(final_path): os.remove(final_path)
|
||||||
os.rename(tmp_path, final_path)
|
os.rename(tmp_path, final_path)
|
||||||
|
|
||||||
except DownloadCancelled:
|
except DownloadCancelled:
|
||||||
@@ -433,7 +411,7 @@ async def handle_audio(callback: CallbackQuery):
|
|||||||
logger.error(f"Error sending audio: {e}")
|
logger.error(f"Error sending audio: {e}")
|
||||||
await status.edit_text("❌ Ошибка при отправке аудио")
|
await status.edit_text("❌ Ошибка при отправке аудио")
|
||||||
|
|
||||||
# ---------------------- PLAYLIST HANDLERS ------------------
|
# ---------------- Playlist Handlers ----------------
|
||||||
@dp.callback_query(F.data == "playlist_all")
|
@dp.callback_query(F.data == "playlist_all")
|
||||||
async def handle_playlist_all(callback: CallbackQuery):
|
async def handle_playlist_all(callback: CallbackQuery):
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
@@ -596,7 +574,6 @@ async def cancel_download(callback: CallbackQuery):
|
|||||||
else:
|
else:
|
||||||
await callback.answer("❌ Нет активной загрузки", show_alert=True)
|
await callback.answer("❌ Нет активной загрузки", show_alert=True)
|
||||||
|
|
||||||
# ---------------------- ENTRYPOINT ------------------
|
|
||||||
async def main():
|
async def main():
|
||||||
cleanup_tmp(TMP_DIR)
|
cleanup_tmp(TMP_DIR)
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user