bug vix1
This commit is contained in:
16
bot/cache.py
16
bot/cache.py
@@ -1,17 +1,7 @@
|
||||
import hashlib
|
||||
|
||||
|
||||
def cache_key(url: str, quality: str, audio: bool = False) -> str:
|
||||
"""Генерирует ключ кэша на основе URL, качества и типа"""
|
||||
key_str = f"{url}_{quality}_{audio}"
|
||||
return hashlib.sha256(key_str.encode()).hexdigest()
|
||||
|
||||
|
||||
# cache.py
|
||||
import os
|
||||
def cache_path(cache_dir: str, key: str, extension: str) -> str:
|
||||
"""Создает путь к файлу в кэше"""
|
||||
# Создаем вложенную структуру для лучшей организации
|
||||
subdir = key[:2]
|
||||
import os
|
||||
full_dir = os.path.join(cache_dir, subdir)
|
||||
os.makedirs(full_dir, exist_ok=True)
|
||||
os.makedirs(full_dir, exist_ok=True) # ← ОБЯЗАТЕЛЬНО
|
||||
return os.path.join(full_dir, f"{key}.{extension}")
|
||||
@@ -4,7 +4,8 @@ import threading
|
||||
import os
|
||||
import logging
|
||||
import shutil
|
||||
from typing import List, Optional
|
||||
import subprocess
|
||||
from typing import List
|
||||
|
||||
from config import (
|
||||
DEFAULT_TIKTOK_VIDEO_QUALITY,
|
||||
@@ -14,24 +15,114 @@ from config import (
|
||||
|
||||
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
|
||||
|
||||
# ---------------- TIKTOK: Видео + Аудио (автоматически) --------------
|
||||
def download_tiktok_video_and_audio(
|
||||
url: str,
|
||||
video_path: str,
|
||||
audio_path: str,
|
||||
cookies: str | None,
|
||||
cancel_event: threading.Event,
|
||||
progress_cb,
|
||||
):
|
||||
# 1. Скачиваем видео (1080p) — УБРАН skip_impersonation
|
||||
video_opts = {
|
||||
"format": f"bestvideo[height<={DEFAULT_TIKTOK_VIDEO_QUALITY}]+bestaudio/best",
|
||||
"outtmpl": video_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,
|
||||
}
|
||||
|
||||
# ---------------- STANDARD VIDEO (YouTube/VK) --------------
|
||||
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 192kbps) — УБРАН skip_impersonation
|
||||
audio_opts = {
|
||||
"format": "bestaudio/best",
|
||||
"outtmpl": audio_path.replace('.mp3', ''),
|
||||
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
||||
"postprocessors": [{
|
||||
"key": "FFmpegExtractAudio",
|
||||
"preferredcodec": "mp3",
|
||||
"preferredquality": DEFAULT_TIKTOK_AUDIO_QUALITY,
|
||||
}],
|
||||
"quiet": True,
|
||||
"no_warnings": False, # ← ВКЛЮЧИЛ для отладки
|
||||
"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")
|
||||
|
||||
# ---------------- INSTAGRAM: Видео в 1080p (автоматически) --------------
|
||||
def download_instagram_video(
|
||||
url: str,
|
||||
out_path: str,
|
||||
cookies: str | None,
|
||||
cancel_event: threading.Event,
|
||||
progress_cb,
|
||||
):
|
||||
# УБРАН skip_impersonation
|
||||
ydl_opts = {
|
||||
"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,
|
||||
@@ -44,178 +135,17 @@ def download_video(
|
||||
"format": f"best[height<={quality}]/best",
|
||||
"outtmpl": out_path,
|
||||
"merge_output_format": "mp4",
|
||||
"cookiefile": cookies,
|
||||
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
||||
"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,
|
||||
@@ -224,12 +154,11 @@ def download_playlist_videos(
|
||||
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,
|
||||
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
||||
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
@@ -239,99 +168,45 @@ def download_playlist_videos(
|
||||
"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")
|
||||
|
||||
total = len(entries)
|
||||
logger.info(f"Downloading playlist: {total} 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")
|
||||
logger.warning(f"Entry {i} has no URL")
|
||||
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}")
|
||||
|
||||
info = ydl.extract_info(entry['url'], download=True)
|
||||
if info and os.path.exists(ydl.prepare_filename(info)):
|
||||
downloaded_files.append(ydl.prepare_filename(info))
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading video {i} ({video_title}): {e}")
|
||||
logger.error(f"Error downloading video {i}: {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}")
|
||||
logger.error(f"Playlist download error: {e}")
|
||||
raise
|
||||
|
||||
|
||||
# ---------------- METADATA (осталось без изменений) --------------
|
||||
|
||||
def add_metadata_to_audio(
|
||||
input_path: str,
|
||||
output_path: str,
|
||||
metadata: dict,
|
||||
):
|
||||
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]}'])
|
||||
|
||||
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
|
||||
'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
|
||||
)
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
if result.returncode != 0:
|
||||
shutil.copy2(input_path, output_path)
|
||||
255
bot/main.py
255
bot/main.py
@@ -3,13 +3,12 @@ import os
|
||||
import asyncio
|
||||
import threading
|
||||
import logging
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from aiogram import Bot, Dispatcher, F
|
||||
from aiogram.types import Message, CallbackQuery, FSInputFile, InputMediaDocument
|
||||
from aiogram.types import Message, CallbackQuery, FSInputFile
|
||||
from aiogram.client.session.aiohttp import AiohttpSession
|
||||
from aiogram.client.telegram import TelegramAPIServer
|
||||
|
||||
@@ -19,7 +18,6 @@ from config import (
|
||||
CACHE_DIR,
|
||||
TMP_DIR,
|
||||
COOKIES_FILE,
|
||||
RATE_LIMIT_SECONDS,
|
||||
CACHE_MAX_AGE_DAYS,
|
||||
CACHE_MAX_SIZE_MB,
|
||||
)
|
||||
@@ -29,25 +27,19 @@ from downloader import (
|
||||
download_instagram_video,
|
||||
download_video,
|
||||
download_audio,
|
||||
download_original_quality,
|
||||
download_playlist_videos,
|
||||
DownloadCancelled,
|
||||
)
|
||||
from middleware import PrivateMiddleware
|
||||
from rate_limit import check_rate_limit
|
||||
from info import extract_info, is_playlist, get_platform_info
|
||||
from cache import cache_key, cache_path
|
||||
from cache import cache_path
|
||||
from cleanup import cleanup_tmp
|
||||
|
||||
# Настройка логирования
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Создаем директории
|
||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||
os.makedirs(TMP_DIR, exist_ok=True)
|
||||
|
||||
# Инициализация бота
|
||||
# Инициализация
|
||||
if LOCAL_API_URL:
|
||||
api_server = TelegramAPIServer.from_base(LOCAL_API_URL)
|
||||
session = AiohttpSession(api=api_server)
|
||||
@@ -56,8 +48,6 @@ else:
|
||||
bot = Bot(token=BOT_TOKEN)
|
||||
|
||||
dp = Dispatcher()
|
||||
|
||||
# Регистрация middleware
|
||||
private_middleware = PrivateMiddleware()
|
||||
dp.message.middleware(private_middleware)
|
||||
dp.callback_query.middleware(private_middleware)
|
||||
@@ -66,14 +56,11 @@ USER_URLS: dict[int, str] = {}
|
||||
USER_DATA: dict[int, dict] = {}
|
||||
ACTIVE_DOWNLOADS: dict[int, dict] = {}
|
||||
|
||||
|
||||
# -------------------- HELPERS --------------
|
||||
|
||||
# Вспомогательные функции
|
||||
def render_bar(percent: float, size: int = 10) -> str:
|
||||
filled = int(size * percent / 100)
|
||||
return "█" * filled + "░" * (size - filled)
|
||||
|
||||
|
||||
def make_progress_cb(loop, message):
|
||||
last_percent = {"value": 0}
|
||||
last_update = {"time": 0}
|
||||
@@ -84,51 +71,40 @@ def make_progress_cb(loop, message):
|
||||
total = d.get("total_bytes") or d.get("total_bytes_estimate") or 1
|
||||
if total <= 0:
|
||||
return
|
||||
|
||||
percent = min(100, downloaded * 100 / total)
|
||||
current_time = time.time()
|
||||
if percent - last_percent["value"] < 2 and current_time - last_update["time"] < 2:
|
||||
return
|
||||
|
||||
last_percent["value"] = percent
|
||||
last_update["time"] = current_time
|
||||
|
||||
bar = render_bar(percent)
|
||||
eta = d.get("eta")
|
||||
eta_str = str(int(float(eta))) if eta and eta != "?" else "?"
|
||||
|
||||
text = (
|
||||
"⏬ <b>Загрузка</b>\n"
|
||||
f"<code>{bar}</code> {percent:.0f}%\n"
|
||||
f"⏱ Осталось: {eta_str} сек"
|
||||
)
|
||||
|
||||
await message.edit_text(
|
||||
text,
|
||||
reply_markup=cancel_keyboard() if "playlist" not in message.text.lower() else None,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
bar = render_bar(percent)
|
||||
text = f"⏬ <b>Загрузка</b>\n<code>{bar}</code> {percent:.0f}%\n⏱ Осталось: {eta_str} сек"
|
||||
await message.edit_text(text, reply_markup=cancel_keyboard() if "playlist" in message.text.lower() else None, parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating progress: {e}")
|
||||
logger.error(f"Progress error: {e}")
|
||||
|
||||
def cb(d):
|
||||
asyncio.run_coroutine_threadsafe(update(d), loop)
|
||||
|
||||
return cb
|
||||
|
||||
# ---------------------- AUTO DOWNLOADS (TikTok/Instagram) ----------------------
|
||||
|
||||
async def process_tiktok_auto(message: Message, user_id: int, url: str):
|
||||
"""Автоматически загружает TikTok: видео (1080p) + аудио (MP3)"""
|
||||
status = await message.answer("🎵 <b>Загружаю TikTok (видео + аудио)...</b>", parse_mode="HTML")
|
||||
|
||||
key_video = cache_key(url, "tiktok_video", audio=False)
|
||||
key_audio = cache_key(url, "tiktok_audio", audio=True)
|
||||
|
||||
# ✅ ИСПРАВЛЕНО: создаём подкаталоги в TMP
|
||||
video_cache = cache_path(CACHE_DIR, key_video, "mp4")
|
||||
audio_cache = cache_path(CACHE_DIR, key_audio, "mp3")
|
||||
tmp_video = cache_path(TMP_DIR, key_video, "mp4")
|
||||
os.makedirs(os.path.dirname(tmp_video), exist_ok=True)
|
||||
|
||||
tmp_video = os.path.join(TMP_DIR, f"{key_video}.mp4")
|
||||
tmp_audio = os.path.join(TMP_DIR, f"{key_audio}.mp3")
|
||||
audio_cache = cache_path(CACHE_DIR, key_audio, "mp3")
|
||||
tmp_audio = cache_path(TMP_DIR, key_audio, "mp3")
|
||||
os.makedirs(os.path.dirname(tmp_audio), exist_ok=True)
|
||||
|
||||
# Проверка кэша
|
||||
if os.path.exists(video_cache) and os.path.exists(audio_cache):
|
||||
@@ -168,14 +144,14 @@ async def process_tiktok_auto(message: Message, user_id: int, url: str):
|
||||
if os.path.exists(p): os.remove(p)
|
||||
return
|
||||
|
||||
# Перемещаем в кэш
|
||||
os.makedirs(os.path.dirname(video_cache), exist_ok=True)
|
||||
# Перемещение в кэш
|
||||
for path in [video_cache, audio_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_audio, audio_cache)
|
||||
|
||||
# Отправка
|
||||
await status.edit_text("📤 <b>Отправляю видео + аудио...</b>", parse_mode="HTML")
|
||||
await message.answer_video(FSInputFile(video_cache), supports_streaming=True)
|
||||
await message.answer_audio(FSInputFile(audio_cache))
|
||||
@@ -189,14 +165,13 @@ async def process_tiktok_auto(message: Message, user_id: int, url: str):
|
||||
ACTIVE_DOWNLOADS.pop(user_id, None)
|
||||
cleanup_tmp(TMP_DIR)
|
||||
|
||||
|
||||
async def process_instagram_auto(message: Message, user_id: int, url: str):
|
||||
"""Автоматически загружает Instagram: видео в 1080p"""
|
||||
status = await message.answer("📸 <b>Загружаю Instagram (1080p)...</b>", parse_mode="HTML")
|
||||
|
||||
key = cache_key(url, "instagram_video", audio=False)
|
||||
final_cache = cache_path(CACHE_DIR, key, "mp4")
|
||||
tmp_path = os.path.join(TMP_DIR, f"{key}.mp4")
|
||||
tmp_path = cache_path(TMP_DIR, key, "mp4")
|
||||
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
|
||||
|
||||
# Проверка кэша
|
||||
if os.path.exists(final_cache):
|
||||
@@ -232,9 +207,9 @@ async def process_instagram_auto(message: Message, user_id: int, url: str):
|
||||
if os.path.exists(tmp_path): os.remove(tmp_path)
|
||||
return
|
||||
|
||||
# Перемещаем в кэш
|
||||
os.makedirs(os.path.dirname(final_cache), exist_ok=True)
|
||||
# Перемещение в кэш
|
||||
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)
|
||||
|
||||
await status.edit_text("📤 <b>Отправляю видео...</b>", parse_mode="HTML")
|
||||
@@ -249,32 +224,28 @@ async def process_instagram_auto(message: Message, user_id: int, url: str):
|
||||
ACTIVE_DOWNLOADS.pop(user_id, None)
|
||||
cleanup_tmp(TMP_DIR)
|
||||
|
||||
|
||||
# -------------------- HANDLERS --------------
|
||||
# ---------------------- HANDLERS ----------------------
|
||||
|
||||
@dp.message(F.text == "/start")
|
||||
async def start(message: Message):
|
||||
await message.answer(
|
||||
"👋 <b>Привет!</b>\n\n"
|
||||
"📥 Я скачиваю <b>видео</b> и <b>звук из видео</b> по ссылке.\n\n"
|
||||
"📥 Скачиваю видео и аудио по ссылке.\n\n"
|
||||
"✨ <b>Новые возможности:</b>\n"
|
||||
"• 🎬 TikTok: автоматически видео + аудио\n"
|
||||
"• 📸 Instagram: автоматически видео в 1080p\n"
|
||||
"• 🎵 Отдельный звук из видео\n"
|
||||
"• 🎵 Аудио из любого видео\n"
|
||||
"• 📁 Плейлисты YouTube\n"
|
||||
"• 🔄 Автоочистка кэша\n\n"
|
||||
"👉 Просто отправь ссылку.",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
@dp.message(F.text.startswith("http"))
|
||||
async def handle_link(message: Message):
|
||||
url = message.text.strip()
|
||||
user_id = message.from_user.id
|
||||
USER_URLS[user_id] = url
|
||||
|
||||
# Определяем платформу
|
||||
platform_info = await asyncio.to_thread(get_platform_info, url)
|
||||
|
||||
# TikTok — автоматически
|
||||
@@ -304,8 +275,7 @@ async def handle_link(message: Message):
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
# ------------------------ YouTube/VK handlers ------------------------
|
||||
# ---------------------- YOUTUBE/VK HANDLERS ----------------------
|
||||
|
||||
@dp.callback_query(F.data.startswith("q:"))
|
||||
async def handle_video(callback: CallbackQuery):
|
||||
@@ -323,19 +293,16 @@ async def handle_video(callback: CallbackQuery):
|
||||
|
||||
key = cache_key(url, quality, audio=False)
|
||||
final_path = cache_path(CACHE_DIR, key, "mp4")
|
||||
tmp_path = os.path.join(TMP_DIR, f"{key}.mp4")
|
||||
optimized_path = os.path.join(TMP_DIR, f"{key}_optimized.mp4")
|
||||
tmp_path = cache_path(TMP_DIR, key, "mp4")
|
||||
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
|
||||
|
||||
# Проверяем кэш
|
||||
# Проверка кэша
|
||||
if os.path.exists(final_path):
|
||||
await status.edit_text("📤 <b>Отправляю файл из кэша…</b>", parse_mode="HTML")
|
||||
try:
|
||||
await callback.message.answer_video(FSInputFile(final_path))
|
||||
size_mb = os.path.getsize(final_path) / 1024 / 1024
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
size_mb = os.path.getsize(final_path) / (1024 * 1024)
|
||||
await callback.message.answer(f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending cached file: {e}")
|
||||
await status.edit_text("❌ Ошибка при отправке файла")
|
||||
@@ -358,31 +325,22 @@ async def handle_video(callback: CallbackQuery):
|
||||
)
|
||||
|
||||
if cancel_event.is_set():
|
||||
for p in [tmp_path, optimized_path]:
|
||||
if os.path.exists(p): os.remove(p)
|
||||
if os.path.exists(tmp_path): os.remove(tmp_path)
|
||||
await status.edit_text("⛔ Загрузка отменена")
|
||||
return
|
||||
|
||||
# Оптимизируем видео для Telegram (кроме оригинального качества)
|
||||
if quality != "original":
|
||||
await status.edit_text("⚙️ <b>Оптимизирую видео для телеграма…</b>", parse_mode="HTML")
|
||||
await asyncio.to_thread(optimize_for_telegram, tmp_path, optimized_path)
|
||||
for p in [tmp_path, optimized_path]:
|
||||
if os.path.exists(p): os.rename(p, optimized_path)
|
||||
else:
|
||||
os.rename(tmp_path, final_path)
|
||||
|
||||
os.rename(optimized_path, 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)
|
||||
|
||||
except DownloadCancelled:
|
||||
for p in [tmp_path, optimized_path]:
|
||||
if os.path.exists(p): os.remove(p)
|
||||
if os.path.exists(tmp_path): os.remove(tmp_path)
|
||||
await status.edit_text("⛔ Загрузка отменена")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading video: {e}")
|
||||
for p in [tmp_path, optimized_path]:
|
||||
if os.path.exists(p): os.remove(p)
|
||||
if os.path.exists(tmp_path): os.remove(tmp_path)
|
||||
await status.edit_text("❌ Не удалось скачать видео")
|
||||
return
|
||||
finally:
|
||||
@@ -391,24 +349,17 @@ async def handle_video(callback: CallbackQuery):
|
||||
try:
|
||||
await callback.message.answer_video(FSInputFile(final_path), supports_streaming=True)
|
||||
size_mb = os.path.getsize(final_path) / (1024 * 1024)
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.message.answer(f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending video: {e}")
|
||||
try:
|
||||
await callback.message.answer_document(FSInputFile(final_path))
|
||||
size_mb = os.path.getsize(final_path) / (1024 * 1024)
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Отправлено как документ</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.message.answer(f"✅ <b>Отправлено как документ</b>\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML")
|
||||
except Exception as e2:
|
||||
logger.error(f"Error sending as document: {e2}")
|
||||
await status.edit_text("❌ Ошибка при отправке файла")
|
||||
|
||||
|
||||
@dp.callback_query(F.data == "audio")
|
||||
async def handle_audio(callback: CallbackQuery):
|
||||
await callback.answer()
|
||||
@@ -424,22 +375,16 @@ async def handle_audio(callback: CallbackQuery):
|
||||
|
||||
key = cache_key(url, "audio", audio=True)
|
||||
final_path = cache_path(CACHE_DIR, key, "mp3")
|
||||
tmp_path = os.path.join(TMP_DIR, f"{key}.mp3")
|
||||
tmp_path = cache_path(TMP_DIR, key, "mp3")
|
||||
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
|
||||
|
||||
# Создаем директории
|
||||
os.makedirs(TMP_DIR, exist_ok=True)
|
||||
os.makedirs(os.path.dirname(final_path), exist_ok=True)
|
||||
|
||||
# Проверяем кэш
|
||||
# Проверка кэша
|
||||
if os.path.exists(final_path):
|
||||
await status.edit_text("📤 <b>Отправляю аудио из кэша…</b>", parse_mode="HTML")
|
||||
try:
|
||||
await callback.message.answer_audio(FSInputFile(final_path))
|
||||
size_mb = os.path.getsize(final_path) / (1024 * 1024)
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.message.answer(f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending cached audio: {e}")
|
||||
await status.edit_text("❌ Ошибка при отправке аудио")
|
||||
@@ -467,32 +412,10 @@ async def handle_audio(callback: CallbackQuery):
|
||||
|
||||
if not os.path.exists(tmp_path):
|
||||
raise Exception("Аудио файл не был создан")
|
||||
|
||||
file_size = os.path.getsize(tmp_path)
|
||||
if file_size == 0:
|
||||
if os.path.getsize(tmp_path) == 0:
|
||||
os.remove(tmp_path)
|
||||
raise Exception("Создан пустой аудио файл")
|
||||
|
||||
# Добавляем метаданные
|
||||
video_info = None
|
||||
try:
|
||||
video_info = await asyncio.to_thread(extract_info, url, COOKIES_FILE)
|
||||
except:
|
||||
pass
|
||||
|
||||
if video_info:
|
||||
metadata = {
|
||||
'title': video_info.get('title', ''),
|
||||
'artist': video_info.get('uploader', ''),
|
||||
'album': video_info.get('title', '')[:50],
|
||||
}
|
||||
await asyncio.to_thread(
|
||||
lambda: add_metadata_to_audio(tmp_path, tmp_path + "_meta.mp3", metadata)
|
||||
)
|
||||
if os.path.exists(tmp_path + "_meta.mp3"):
|
||||
os.remove(tmp_path)
|
||||
os.rename(tmp_path + "_meta.mp3", tmp_path)
|
||||
|
||||
os.rename(tmp_path, final_path)
|
||||
|
||||
except DownloadCancelled:
|
||||
@@ -508,47 +431,35 @@ async def handle_audio(callback: CallbackQuery):
|
||||
ACTIVE_DOWNLOADS.pop(user_id, None)
|
||||
|
||||
await status.edit_text("📤 <b>Отправляю аудио…</b>", parse_mode="HTML")
|
||||
|
||||
try:
|
||||
await callback.message.answer_audio(FSInputFile(final_path))
|
||||
size_mb = os.path.getsize(final_path) / (1024 * 1024)
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.message.answer(f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending audio: {e}")
|
||||
await status.edit_text("❌ Ошибка при отправке аудио")
|
||||
|
||||
|
||||
# -------------------- PLAYLIST HANDLERS --------------
|
||||
|
||||
# ---------------------- PLAYLIST HANDLERS ----------------------
|
||||
@dp.callback_query(F.data == "playlist_all")
|
||||
async def handle_playlist_all(callback: CallbackQuery):
|
||||
await callback.answer()
|
||||
user_id = callback.from_user.id
|
||||
url = USER_URLS.get(user_id)
|
||||
|
||||
if not url:
|
||||
await callback.message.answer("❌ Ссылка не найдена")
|
||||
return
|
||||
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
status = await callback.message.answer("📁 <b>Анализирую плейлист…</b>", parse_mode="HTML")
|
||||
|
||||
try:
|
||||
from info import get_playlist_info
|
||||
playlist_info = await asyncio.to_thread(get_playlist_info, url)
|
||||
|
||||
if not playlist_info or 'entries' not in playlist_info:
|
||||
await status.edit_text("❌ Не удалось получить информацию о плейлисте")
|
||||
return
|
||||
|
||||
video_count = len(playlist_info['entries'])
|
||||
if video_count == 0:
|
||||
await status.edit_text("❌ Плейлист пуст")
|
||||
return
|
||||
|
||||
if video_count > 10:
|
||||
await callback.message.answer(
|
||||
f"⚠️ <b>Внимание!</b>\n\n"
|
||||
@@ -560,55 +471,43 @@ async def handle_playlist_all(callback: CallbackQuery):
|
||||
)
|
||||
USER_DATA[user_id] = {"playlist_info": playlist_info, "status_message": status}
|
||||
return
|
||||
|
||||
await download_playlist_confirm(callback, user_id, playlist_info, status)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing playlist: {e}")
|
||||
await status.edit_text("❌ Ошибка при анализе плейлиста")
|
||||
|
||||
|
||||
@dp.callback_query(F.data == "playlist_confirm_yes")
|
||||
async def handle_playlist_confirm(callback: CallbackQuery):
|
||||
await callback.answer()
|
||||
user_id = callback.from_user.id
|
||||
|
||||
data = USER_DATA.get(user_id, {})
|
||||
playlist_info = data.get("playlist_info")
|
||||
status = data.get("status_message")
|
||||
|
||||
if not playlist_info or not status:
|
||||
await callback.message.answer("❌ Данные плейлиста не найдены")
|
||||
return
|
||||
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
await download_playlist_confirm(callback, user_id, playlist_info, status)
|
||||
|
||||
|
||||
async def download_playlist_confirm(callback, user_id, playlist_info, status):
|
||||
import uuid
|
||||
import shutil
|
||||
video_count = len(playlist_info['entries'])
|
||||
playlist_title = playlist_info.get('title', 'Плейлист')
|
||||
await status.edit_text(
|
||||
f"📁 <b>Начинаю загрузку плейлиста</b>\n\n"
|
||||
f"🎬 Название: {playlist_title}\n"
|
||||
f"📹 Видео: {video_count}\n"
|
||||
f"⏳ Подготовка...",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
cancel_event = threading.Event()
|
||||
ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event}
|
||||
loop = asyncio.get_running_loop()
|
||||
progress_cb = make_progress_cb(loop, status)
|
||||
playlist_dir = os.path.join(TMP_DIR, f"playlist_{uuid.uuid4().hex[:8]}")
|
||||
os.makedirs(playlist_dir, exist_ok=True)
|
||||
try:
|
||||
import uuid
|
||||
import shutil
|
||||
|
||||
video_count = len(playlist_info['entries'])
|
||||
playlist_title = playlist_info.get('title', 'Плейлист')
|
||||
|
||||
await status.edit_text(
|
||||
f"📁 <b>Начинаю загрузку плейлиста</b>\n\n"
|
||||
f"🎬 Название: {playlist_title}\n"
|
||||
f"📹 Видео: {video_count}\n"
|
||||
f"⏳ Подготовка...",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
cancel_event = threading.Event()
|
||||
ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event}
|
||||
loop = asyncio.get_running_loop()
|
||||
progress_cb = make_progress_cb(loop, status)
|
||||
|
||||
playlist_dir = os.path.join(TMP_DIR, f"playlist_{uuid.uuid4().hex[:8]}")
|
||||
os.makedirs(playlist_dir, exist_ok=True)
|
||||
|
||||
downloaded_files = await asyncio.to_thread(
|
||||
download_playlist_videos,
|
||||
playlist_info,
|
||||
@@ -617,26 +516,20 @@ async def download_playlist_confirm(callback, user_id, playlist_info, status):
|
||||
cancel_event,
|
||||
progress_cb
|
||||
)
|
||||
|
||||
if cancel_event.is_set():
|
||||
await status.edit_text("⛔ Загрузка плейлиста отменена")
|
||||
shutil.rmtree(playlist_dir, ignore_errors=True)
|
||||
return
|
||||
|
||||
if not downloaded_files:
|
||||
await status.edit_text("❌ Не удалось загрузить видео из плейлиста")
|
||||
shutil.rmtree(playlist_dir, ignore_errors=True)
|
||||
return
|
||||
|
||||
await status.edit_text(f"📤 <b>Отправляю {len(downloaded_files)} видео…</b>", parse_mode="HTML")
|
||||
|
||||
downloaded_files.sort(key=lambda x: os.path.getsize(x))
|
||||
sent_count = 0
|
||||
|
||||
for i, file_path in enumerate(downloaded_files, 1):
|
||||
if cancel_event.is_set():
|
||||
break
|
||||
|
||||
try:
|
||||
file_name = os.path.basename(file_path)
|
||||
display_name = os.path.splitext(file_name)[0]
|
||||
@@ -649,10 +542,8 @@ async def download_playlist_confirm(callback, user_id, playlist_info, status):
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending file {file_path}: {e}")
|
||||
continue
|
||||
|
||||
total_size = sum(os.path.getsize(f) for f in downloaded_files)
|
||||
total_size_mb = total_size / (1024 * 1024)
|
||||
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Плейлист загружен!</b>\n\n"
|
||||
f"📁 Видео в плейлисте: {video_count}\n"
|
||||
@@ -661,9 +552,7 @@ async def download_playlist_confirm(callback, user_id, playlist_info, status):
|
||||
f"🎬 Название: {playlist_title}",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
shutil.rmtree(playlist_dir, ignore_errors=True)
|
||||
|
||||
except DownloadCancelled:
|
||||
await status.edit_text("⛔ Загрузка плейлиста отменена")
|
||||
except Exception as e:
|
||||
@@ -673,30 +562,24 @@ async def download_playlist_confirm(callback, user_id, playlist_info, status):
|
||||
ACTIVE_DOWNLOADS.pop(user_id, None)
|
||||
cleanup_tmp(TMP_DIR)
|
||||
|
||||
|
||||
@dp.callback_query(F.data == "playlist_confirm_no")
|
||||
async def handle_playlist_cancel(callback: CallbackQuery):
|
||||
await callback.answer("Отменено", show_alert=True)
|
||||
|
||||
|
||||
@dp.callback_query(F.data == "playlist_first")
|
||||
async def handle_playlist_first(callback: CallbackQuery):
|
||||
await callback.answer()
|
||||
user_id = callback.from_user.id
|
||||
url = USER_URLS.get(user_id)
|
||||
|
||||
if not url:
|
||||
await callback.message.answer("❌ Ссылка не найдена")
|
||||
return
|
||||
|
||||
try:
|
||||
from info import get_first_video_from_playlist
|
||||
video_url = await asyncio.to_thread(get_first_video_from_playlist, url)
|
||||
|
||||
if not video_url:
|
||||
await callback.message.answer("❌ Не удалось получить видео из плейлиста")
|
||||
return
|
||||
|
||||
USER_URLS[user_id] = video_url
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
await callback.message.answer(
|
||||
@@ -704,17 +587,14 @@ async def handle_playlist_first(callback: CallbackQuery):
|
||||
reply_markup=youtube_quality_keyboard(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting first video: {e}")
|
||||
await callback.message.answer("❌ Ошибка при получении видео из плейлиста")
|
||||
|
||||
|
||||
@dp.callback_query(F.data == "cancel")
|
||||
async def cancel_download(callback: CallbackQuery):
|
||||
user_id = callback.from_user.id
|
||||
data = ACTIVE_DOWNLOADS.get(user_id)
|
||||
|
||||
if data:
|
||||
data["cancel"].set()
|
||||
ACTIVE_DOWNLOADS.pop(user_id, None)
|
||||
@@ -722,19 +602,14 @@ async def cancel_download(callback: CallbackQuery):
|
||||
else:
|
||||
await callback.answer("❌ Нет активной загрузки", show_alert=True)
|
||||
|
||||
|
||||
# -------------------- ENTRYPOINT --------------
|
||||
|
||||
# ---------------------- ENTRYPOINT ----------------------
|
||||
async def main():
|
||||
# Очистка временных файлов при старте
|
||||
cleanup_tmp(TMP_DIR)
|
||||
|
||||
try:
|
||||
await dp.start_polling(bot)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Bot stopped by user")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
|
||||
Reference in New Issue
Block a user