From c7ef7cdd57dbe1e564459593abf8d4f5e949a9e4 Mon Sep 17 00:00:00 2001 From: smolkik-code Date: Wed, 18 Feb 2026 14:05:36 +0700 Subject: [PATCH 1/6] update to 2.0 --- .DS_Store | Bin 16388 -> 16388 bytes bot/config.py | 13 +- bot/downloader.py | 404 ++++++++------- bot/keyboards.py | 53 +- bot/main.py | 976 ++++++++++-------------------------- wireguard/wg_confs/wg0.conf | 11 + 6 files changed, 507 insertions(+), 950 deletions(-) create mode 100644 wireguard/wg_confs/wg0.conf diff --git a/.DS_Store b/.DS_Store index fec16823d36adab09a514469f7e77520a1f200cd..46d352977d5e59d2ac7753f36f05111047a7ce12 100644 GIT binary patch delta 274 zcmZo^U~Fk%+^|+&OS-z+(A>yGM?u%nz`&wbN1@u%$UsNI(!j8`mXkwNS>HM+K07Bj zFTZ#425EV|J_ZH`W`-n&e1?*o;H-{-4QyC8a~mvT OCs8|wJ1kaGXaoS*AxU}w delta 88 zcmZo^U~Fk%+^|-D@>B(H9%hClhJ1#SoZ>R)q|Gb}=IoP|74~larOwMSS List[str]: - """Скачивает все видео из плейлиста""" - downloaded_files = [] - - ydl_opts = { - "format": "best[height<=1080]/best", - "outtmpl": os.path.join(output_dir, "%(title)s [%(id)s].%(ext)s"), - "merge_output_format": "mp4", - "cookiefile": cookies, - "progress_hooks": [_progress_hook(cancel_event, progress_cb)], - "quiet": True, - "no_warnings": True, - "ignoreerrors": True, - "extract_flat": False, - "nooverwrites": True, - "concurrent_fragment_downloads": 4, - "http_chunk_size": 10485760, - } - - try: - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - entries = playlist_info.get('entries', []) - total_videos = len(entries) - - logger.info(f"Starting playlist download with {total_videos} videos") - - for i, entry in enumerate(entries, 1): - if cancel_event.is_set(): - raise DownloadCancelled("Cancelled by user") - - if not entry.get('url'): - logger.warning(f"Entry {i} has no URL, skipping") - continue - - video_url = entry['url'] - video_title = entry.get('title', f'Video {i}') - - logger.info(f"Downloading video {i}/{total_videos}: {video_title}") - - try: - # Загружаем видео - info = ydl.extract_info(video_url, download=True) - - if not info: - logger.error(f"Failed to extract info for video {i}") - continue - - # Получаем имя фактически созданного файла - filename = ydl.prepare_filename(info) - - # Проверяем существование файла - if os.path.exists(filename): - downloaded_files.append(filename) - logger.info(f"Successfully downloaded: {os.path.basename(filename)}") - else: - # Ищем файл с другим расширением - base_name = filename.rsplit('.', 1)[0] - for ext in ['.mp4', '.mkv', '.webm', '.flv']: - alt_path = base_name + ext - if os.path.exists(alt_path): - downloaded_files.append(alt_path) - logger.info(f"Found file with extension {ext}: {os.path.basename(alt_path)}") - break - else: - logger.error(f"File not found for video {i}") - - except Exception as e: - logger.error(f"Error downloading video {i} ({video_title}): {e}") - continue - - logger.info(f"Playlist download complete. Downloaded {len(downloaded_files)} files") - return downloaded_files - - except Exception as e: - logger.error(f"Error downloading playlist: {e}") - raise - - -# ---------------- STANDARD AUDIO ---------------- +# ---------------- AUDIO (для YouTube) -------------- def download_audio( url: str, @@ -294,4 +180,158 @@ def download_audio( } with yt_dlp.YoutubeDL(ydl_opts) as ydl: - ydl.download([url]) \ No newline at end of file + 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) \ No newline at end of file diff --git a/bot/keyboards.py b/bot/keyboards.py index 6a3274c..356b70e 100644 --- a/bot/keyboards.py +++ b/bot/keyboards.py @@ -1,14 +1,16 @@ +# keyboards.py from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton -def quality_keyboard(): - """Клавиатура выбора качества""" +def youtube_quality_keyboard(): + """YouTube/VK: выбор качества""" keyboard = [ [ InlineKeyboardButton(text="📹 480p", callback_data="q:480"), InlineKeyboardButton(text="📹 720p", callback_data="q:720"), + ], + [ InlineKeyboardButton(text="🎬 1080p", callback_data="q:1080"), - InlineKeyboardButton(text="🎥 1440p", callback_data="q:1440"), ], [ InlineKeyboardButton(text="🎧 Аудио", callback_data="audio"), @@ -19,7 +21,7 @@ def quality_keyboard(): def cancel_keyboard(): - """Клавиатура с кнопкой отмены""" + """Клавиатура с кнопкой отмены (для плейлистов)""" keyboard = [ [InlineKeyboardButton(text="⛔ Отменить загрузку", callback_data="cancel")] ] @@ -27,7 +29,7 @@ def cancel_keyboard(): def playlist_keyboard(confirm=False): - """Клавиатура для плейлистов""" + """Клавиатура для плейлистов YouTube""" if confirm: keyboard = [ [ @@ -45,45 +47,4 @@ def playlist_keyboard(confirm=False): InlineKeyboardButton(text="❌ Отмена", callback_data="cancel"), ] ] - return InlineKeyboardMarkup(inline_keyboard=keyboard) - - -def platform_keyboard(platform): - """Клавиатура для Instagram""" - keyboard = [ - [ - InlineKeyboardButton(text=f"🎬 Оригинальное качество", callback_data="original_quality"), - ], - [ - InlineKeyboardButton(text="📹 480p", callback_data="q:480"), - InlineKeyboardButton(text="📹 720p", callback_data="q:720"), - InlineKeyboardButton(text="🎬 1080p", callback_data="q:1080"), - ], - [ - InlineKeyboardButton(text="🎧 Аудио", callback_data="audio"), - InlineKeyboardButton(text="❌ Отмена", callback_data="cancel"), - ] - ] - return InlineKeyboardMarkup(inline_keyboard=keyboard) - - -def tiktok_keyboard(): - """Специальная клавиатура для TikTok""" - keyboard = [ - [ - InlineKeyboardButton(text="🎬 Оригинальное качество", callback_data="original_quality"), - ], - [ - InlineKeyboardButton(text="📹 480p", callback_data="q:480"), - InlineKeyboardButton(text="📹 720p", callback_data="q:720"), - InlineKeyboardButton(text="🎬 1080p", callback_data="q:1080"), - ], - [ - InlineKeyboardButton(text="🎧 Аудио из видео", callback_data="audio"), - InlineKeyboardButton(text="🎵 Только звук (TikTok)", callback_data="tiktok_music"), - ], - [ - InlineKeyboardButton(text="❌ Отмена", callback_data="cancel"), - ] - ] return InlineKeyboardMarkup(inline_keyboard=keyboard) \ No newline at end of file diff --git a/bot/main.py b/bot/main.py index 4575f2b..555a929 100644 --- a/bot/main.py +++ b/bot/main.py @@ -1,3 +1,4 @@ +# main.py import os import asyncio import threading @@ -22,20 +23,14 @@ from config import ( CACHE_MAX_AGE_DAYS, CACHE_MAX_SIZE_MB, ) -from keyboards import ( - quality_keyboard, - cancel_keyboard, - playlist_keyboard, - platform_keyboard, - tiktok_keyboard, -) +from keyboards import youtube_quality_keyboard, cancel_keyboard, playlist_keyboard from downloader import ( - download_video, - download_audio, + download_tiktok_video_and_audio, + download_instagram_video, + download_video, + download_audio, download_original_quality, download_playlist_videos, - download_tiktok_music, - add_metadata_to_audio, # Только для аудио DownloadCancelled, ) from middleware import PrivateMiddleware @@ -44,8 +39,6 @@ from info import extract_info, is_playlist, get_platform_info from cache import cache_key, cache_path from cleanup import cleanup_tmp -# -------------------- init -------------------- - # Настройка логирования logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -54,7 +47,7 @@ logger = logging.getLogger(__name__) os.makedirs(CACHE_DIR, exist_ok=True) os.makedirs(TMP_DIR, exist_ok=True) -# Инициализация бота с локальным API (если используется) +# Инициализация бота if LOCAL_API_URL: api_server = TelegramAPIServer.from_base(LOCAL_API_URL) session = AiohttpSession(api=api_server) @@ -73,120 +66,8 @@ USER_URLS: dict[int, str] = {} USER_DATA: dict[int, dict] = {} ACTIVE_DOWNLOADS: dict[int, dict] = {} -# -------------------- cache cleaning -------------------- -def cleanup_old_cache(): - """ - Очищает старые файлы из кэша - """ - try: - current_time = time.time() - deleted_count = 0 - deleted_size = 0 - - # Удаляем файлы старше CACHE_MAX_AGE_DAYS дней - if CACHE_MAX_AGE_DAYS > 0: - cutoff_time = current_time - (CACHE_MAX_AGE_DAYS * 24 * 3600) - - for root, dirs, files in os.walk(CACHE_DIR): - for file in files: - file_path = os.path.join(root, file) - try: - file_mtime = os.path.getmtime(file_path) - if file_mtime < cutoff_time: - file_size = os.path.getsize(file_path) - os.remove(file_path) - deleted_count += 1 - deleted_size += file_size - logger.info(f"Deleted old cache file: {file}") - except Exception as e: - logger.error(f"Error deleting file {file}: {e}") - - # Если указан максимальный размер кэша, проверяем его - if CACHE_MAX_SIZE_MB > 0: - total_size_mb = get_cache_size_mb() - if total_size_mb > CACHE_MAX_SIZE_MB: - # Сортируем файлы по времени изменения (старые первыми) - files_with_mtime = [] - for root, dirs, files in os.walk(CACHE_DIR): - for file in files: - file_path = os.path.join(root, file) - try: - mtime = os.path.getmtime(file_path) - size = os.path.getsize(file_path) - files_with_mtime.append((file_path, mtime, size)) - except: - pass - - # Сортируем по времени (старые первыми) - files_with_mtime.sort(key=lambda x: x[1]) - - # Удаляем старые файлы пока не достигнем лимита - target_size_mb = CACHE_MAX_SIZE_MB * 0.8 - - for file_path, mtime, size in files_with_mtime: - if total_size_mb <= target_size_mb: - break - - try: - os.remove(file_path) - deleted_count += 1 - deleted_size += size - total_size_mb -= size / (1024 * 1024) - logger.info(f"Deleted cache file to free space: {os.path.basename(file_path)}") - except Exception as e: - logger.error(f"Error deleting file {file_path}: {e}") - - if deleted_count > 0: - logger.info(f"Cache cleanup: deleted {deleted_count} files, freed {deleted_size / (1024*1024):.2f} MB") - else: - logger.info("Cache cleanup: no files to delete") - - except Exception as e: - logger.error(f"Error in cache cleanup: {e}") - - -def get_cache_size_mb(): - """Возвращает размер кэша в МБ""" - total_size = 0 - for root, dirs, files in os.walk(CACHE_DIR): - for file in files: - file_path = os.path.join(root, file) - try: - total_size += os.path.getsize(file_path) - except: - pass - return total_size / (1024 * 1024) - - -async def scheduled_cache_cleanup(): - """Периодическая очистка кэша""" - # Запускаем очистку сразу при старте - logger.info("Running initial cache cleanup...") - await asyncio.to_thread(cleanup_old_cache) - - last_cleanup_day = datetime.now().day - - while True: - try: - now = datetime.now() - current_day = now.day - - # Запускаем очистку если наступил новый день И сейчас между 3:00 и 3:59 - if current_day != last_cleanup_day and now.hour == 3: - logger.info("Starting scheduled cache cleanup...") - await asyncio.to_thread(cleanup_old_cache) - last_cleanup_day = current_day - - # Ждем 5 минут перед следующей проверкой - await asyncio.sleep(300) - - except Exception as e: - logger.error(f"Error in scheduled cleanup: {e}") - await asyncio.sleep(60) - - -# -------------------- helpers -------------------- +# -------------------- HELPERS -------------- def render_bar(percent: float, size: int = 10) -> str: filled = int(size * percent / 100) @@ -201,29 +82,20 @@ def make_progress_cb(loop, message): try: downloaded = d.get("downloaded_bytes", 0) total = d.get("total_bytes") or d.get("total_bytes_estimate") or 1 - if total <= 0: return - - percent = min(100, downloaded * 100 / total) + 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") - - if eta is None or eta == "?": - eta_str = "?" - else: - try: - eta_str = str(int(float(eta))) - except (ValueError, TypeError): - eta_str = "?" + eta_str = str(int(float(eta))) if eta and eta != "?" else "?" text = ( "⏬ Загрузка\n" @@ -233,7 +105,7 @@ def make_progress_cb(loop, message): await message.edit_text( text, - reply_markup=cancel_keyboard(), + reply_markup=cancel_keyboard() if "playlist" not in message.text.lower() else None, parse_mode="HTML" ) except Exception as e: @@ -245,114 +117,140 @@ def make_progress_cb(loop, message): return cb -def make_playlist_progress_cb(loop, message, total_videos: int): - current_video = {"value": 0} - last_update = {"time": 0} +async def process_tiktok_auto(message: Message, user_id: int, url: str): + """Автоматически загружает TikTok: видео (1080p) + аудио (MP3)""" + status = await message.answer("🎵 Загружаю TikTok (видео + аудио)...", parse_mode="HTML") - async def update(d): + key_video = cache_key(url, "tiktok_video", audio=False) + key_audio = cache_key(url, "tiktok_audio", audio=True) + + video_cache = cache_path(CACHE_DIR, key_video, "mp4") + audio_cache = cache_path(CACHE_DIR, key_audio, "mp3") + + tmp_video = os.path.join(TMP_DIR, f"{key_video}.mp4") + tmp_audio = os.path.join(TMP_DIR, f"{key_audio}.mp3") + + # Проверка кэша + if os.path.exists(video_cache) and os.path.exists(audio_cache): + await status.edit_text("📤 Отправляю из кэша...", parse_mode="HTML") try: - current_time = time.time() - if current_time - last_update["time"] < 3: - return - - last_update["time"] = current_time - - if d.get("status") == "finished": - current_video["value"] += 1 - - text = ( - f"📁 Загрузка плейлиста\n" - f"📹 Видео: {current_video['value']}/{total_videos}\n" - f"⏳ Продолжаем загрузку..." - ) - - await message.edit_text( - text, - parse_mode="HTML" - ) + await message.answer_video(FSInputFile(video_cache), supports_streaming=True) + await message.answer_audio(FSInputFile(audio_cache)) + size_mb = (os.path.getsize(video_cache) + os.path.getsize(audio_cache)) / (1024 * 1024) + await message.answer(f"✅ Готово! (TikTok)\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML") except Exception as e: - logger.error(f"Error updating playlist progress: {e}") + logger.error(f"TikTok cache error: {e}") + await status.edit_text("❌ Ошибка при отправке") + finally: + cleanup_tmp(TMP_DIR) + return - def cb(d): - asyncio.run_coroutine_threadsafe(update(d), loop) + # Загрузка + cancel_event = threading.Event() + ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event} + loop = asyncio.get_running_loop() + progress_cb = make_progress_cb(loop, status) - return cb - - -def optimize_for_telegram(input_path: str, output_path: str) -> bool: - """ - Оптимизирует видео для телеграма (без метаданных) - """ try: - import shutil - - # Проверяем размер файла - file_size_mb = os.path.getsize(input_path) / (1024 * 1024) - - # Если файл больше 50 МБ, сжимаем его - if file_size_mb > 50: - crf = 28 - else: - crf = 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=300 + await asyncio.to_thread( + download_tiktok_video_and_audio, + url, + tmp_video, + tmp_audio, + COOKIES_FILE, + cancel_event, + progress_cb, ) - - if result.returncode != 0: - logger.error(f"FFmpeg error: {result.stderr}") - shutil.copy2(input_path, output_path) - return False - - return True - + + if cancel_event.is_set(): + await status.edit_text("⛔ Загрузка отменена") + for p in [tmp_video, tmp_audio]: + 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.rename(tmp_video, video_cache) + os.rename(tmp_audio, audio_cache) + + # Отправка + await status.edit_text("📤 Отправляю видео + аудио...", parse_mode="HTML") + await message.answer_video(FSInputFile(video_cache), supports_streaming=True) + await message.answer_audio(FSInputFile(audio_cache)) + size_mb = (os.path.getsize(video_cache) + os.path.getsize(audio_cache)) / (1024 * 1024) + await message.answer(f"✅ Готово! (TikTok)\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML") + except Exception as e: - logger.error(f"Error optimizing video: {e}") - import shutil - shutil.copy2(input_path, output_path) - return False + logger.error(f"TikTok download error: {e}") + await status.edit_text(f"❌ Ошибка: {str(e)[:120]}") + finally: + ACTIVE_DOWNLOADS.pop(user_id, None) + cleanup_tmp(TMP_DIR) -def get_video_info_for_audio(video_info: dict) -> dict: - """Получает информацию о видео для метаданных аудио""" - if not video_info: - return {} - - metadata = { - 'title': video_info.get('title', ''), - 'artist': video_info.get('uploader', ''), - 'album': video_info.get('title', '')[:50], - 'url': video_info.get('webpage_url', ''), - } - - # Для TikTok добавляем информацию о музыке - if video_info.get('extractor') == 'TikTok': - if video_info.get('track'): - metadata['title'] = video_info.get('track', metadata['title']) - if video_info.get('artist'): - metadata['artist'] = video_info.get('artist', metadata['artist']) - - return metadata +async def process_instagram_auto(message: Message, user_id: int, url: str): + """Автоматически загружает Instagram: видео в 1080p""" + status = await message.answer("📸 Загружаю Instagram (1080p)...", 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") + + # Проверка кэша + if os.path.exists(final_cache): + await status.edit_text("📤 Отправляю из кэша...", parse_mode="HTML") + try: + await message.answer_video(FSInputFile(final_cache), supports_streaming=True) + size_mb = os.path.getsize(final_cache) / (1024 * 1024) + await message.answer(f"✅ Готово! (Instagram 1080p)\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML") + except Exception as e: + logger.error(f"Instagram cache error: {e}") + await status.edit_text("❌ Ошибка при отправке") + finally: + cleanup_tmp(TMP_DIR) + return + + cancel_event = threading.Event() + ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event} + loop = asyncio.get_running_loop() + progress_cb = make_progress_cb(loop, status) + + try: + await asyncio.to_thread( + download_instagram_video, + url, + tmp_path, + COOKIES_FILE, + cancel_event, + progress_cb, + ) + + if cancel_event.is_set(): + await status.edit_text("⛔ Загрузка отменена") + 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.rename(tmp_path, final_cache) + + await status.edit_text("📤 Отправляю видео...", parse_mode="HTML") + await message.answer_video(FSInputFile(final_cache), supports_streaming=True) + size_mb = os.path.getsize(final_cache) / (1024 * 1024) + await message.answer(f"✅ Готово! (Instagram 1080p)\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML") + + except Exception as e: + logger.error(f"Instagram download error: {e}") + await status.edit_text(f"❌ Ошибка: {str(e)[:120]}") + finally: + ACTIVE_DOWNLOADS.pop(user_id, None) + cleanup_tmp(TMP_DIR) -# -------------------- handlers -------------------- +# -------------------- HANDLERS -------------- @dp.message(F.text == "/start") async def start(message: Message): @@ -360,360 +258,54 @@ async def start(message: Message): "👋 Привет!\n\n" "📥 Я скачиваю видео и звук из видео по ссылке.\n\n" "✨ Новые возможности:\n" - "• 🎬 Оригинальное качество (Instagram, TikTok)\n" + "• 🎬 TikTok: автоматически видео + аудио\n" + "• 📸 Instagram: автоматически видео в 1080p\n" + "• 🎵 Отдельный звук из видео\n" "• 📁 Плейлисты YouTube\n" - "• 🎵 Отдельный звук для TikTok\n" "• 🔄 Автоочистка кэша\n\n" "👉 Просто отправь ссылку.", parse_mode="HTML" ) -@dp.message(F.text == "/help") -async def help_command(message: Message): - await message.answer( - "📚 Справка по командам:\n\n" - "• /start - Начать работу\n" - "• /help - Эта справка\n" - "• /cache_stats - Статистика кэша\n\n" - "✨ Поддерживаемые платформы:\n" - "• YouTube (видео и плейлисты)\n" - "• TikTok (оригинальное качество + звук отдельно)\n" - "• Instagram (Reels, видео, IGTV)\n" - "• Twitter/X, Facebook, VK и другие\n\n" - "🎯 Особенности:\n" - "• 🎵 TikTok: можно скачать отдельно звук\n" - "• 🎬 Instagram/TikTok: оригинальное качество\n" - "• 📁 Плейлисты: загрузка всех видео\n" - "• 🎧 Аудио: извлечение звука из любого видео", - parse_mode="HTML" - ) - - -@dp.message(F.text == "/cache_stats") -async def cache_stats(message: Message): - """Показывает статистику кэша""" - try: - total_size_mb = get_cache_size_mb() - file_count = 0 - - for root, dirs, files in os.walk(CACHE_DIR): - file_count += len(files) - - await message.answer( - f"📊 Статистика кэша:\n\n" - f"📁 Файлов: {file_count}\n" - f"💾 Размер: {total_size_mb:.2f} МБ\n" - f"⏰ Очистка: ежедневно в 3:00", - parse_mode="HTML" - ) - except Exception as e: - logger.error(f"Error getting cache stats: {e}") - await message.answer("❌ Ошибка при получении статистики кэша") - - @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 показываем специальное меню + + # TikTok — автоматически if platform_info == "tiktok": - USER_DATA[user_id] = {"platform": platform_info} - await message.answer( - f"🎵 Ссылка с TikTok\n\n" - "Выберите что скачать:", - reply_markup=tiktok_keyboard(), - parse_mode="HTML" - ) + await process_tiktok_auto(message, user_id, url) return - - # Проверяем, является ли ссылка плейлистом (кроме TikTok) - try: - is_playlist_url = await asyncio.to_thread(is_playlist, url) - if is_playlist_url: - USER_DATA[user_id] = {"is_playlist": True} - await message.answer( - "📁 Обнаружен плейлист!\n\n" - "Выберите действие:", - reply_markup=playlist_keyboard(), - parse_mode="HTML" - ) - return - except: - pass - - # Для Instagram предлагаем оригинальное качество + + # Instagram — автоматически if platform_info == "instagram": - USER_DATA[user_id] = {"platform": platform_info} + await process_instagram_auto(message, user_id, url) + return + + # Плейлисты + if await asyncio.to_thread(is_playlist, url): await message.answer( - f"📸 Ссылка с Instagram\n\n" - "Выберите качество загрузки:", - reply_markup=platform_keyboard(platform_info), - parse_mode="HTML" - ) - else: - # Для других платформ обычное меню - await message.answer( - "🔽 Выбери формат загрузки:", - reply_markup=quality_keyboard(), + "📁 Обнаружен плейлист!\n\n" + "Выберите действие:", + reply_markup=playlist_keyboard(), parse_mode="HTML" ) + return + + # YouTube/VK — выбор качества + await message.answer( + "🔽 Выбери качество:", + reply_markup=youtube_quality_keyboard(), + parse_mode="HTML" + ) -@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) - await callback.answer("⛔ Загрузка отменена", show_alert=True) - else: - await callback.answer("❌ Нет активной загрузки", show_alert=True) - - -# ---------------- TIKTOK SPECIAL HANDLERS ---------------- - -@dp.callback_query(F.data == "tiktok_music") -async def handle_tiktok_music(callback: CallbackQuery): - """Отдельная загрузка звука из TikTok""" - await callback.answer() - user_id = callback.from_user.id - url = USER_URLS.get(user_id) - - if not url: - await callback.message.answer("❌ Ссылка не найдена") - return - - if not check_rate_limit(user_id, RATE_LIMIT_SECONDS): - await callback.message.answer("⏳ Подожди немного перед следующим запросом") - return - - await callback.message.edit_reply_markup(reply_markup=None) - status = await callback.message.answer("🎵 Извлекаю звук из TikTok…", parse_mode="HTML") - - key = cache_key(url, "tiktok_music", audio=True) - final_path = cache_path(CACHE_DIR, key, "mp3") - tmp_path = os.path.join(TMP_DIR, f"{key}.mp3") - - # Создаем директории если не существуют - 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("📤 Отправляю звук из кэша…", 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"✅ Готово! (Звук из TikTok)\n📦 Размер: {size_mb:.1f} МБ", - parse_mode="HTML" - ) - except Exception as e: - logger.error(f"Error sending cached audio: {e}") - await status.edit_text("❌ Ошибка при отправке звука") - return - - cancel_event = threading.Event() - ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event} - loop = asyncio.get_running_loop() - progress_cb = make_progress_cb(loop, status) - - try: - # Загружаем звук из TikTok - await asyncio.to_thread( - download_tiktok_music, - url, - tmp_path, - COOKIES_FILE, - cancel_event, - progress_cb, - ) - - # Проверяем, был ли отменен процесс - if cancel_event.is_set(): - await status.edit_text("⛔ Загрузка отменена") - if os.path.exists(tmp_path): - os.remove(tmp_path) - return - - # Проверяем, создан ли файл - if not os.path.exists(tmp_path): - raise Exception("Аудио файл не был создан") - - # Проверяем размер файла - file_size = os.path.getsize(tmp_path) - if file_size == 0: - os.remove(tmp_path) - raise Exception("Создан пустой аудио файл") - - # Добавляем метаданные к аудио - try: - info = await asyncio.to_thread(extract_info, url, COOKIES_FILE) - if info: - metadata = get_video_info_for_audio(info) - await asyncio.to_thread(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) - except Exception as e: - logger.error(f"Error adding metadata to audio: {e}") - - # Перемещаем файл в кэш - if os.path.exists(final_path): - os.remove(final_path) - os.rename(tmp_path, final_path) - - except DownloadCancelled: - await status.edit_text("⛔ Загрузка отменена") - if os.path.exists(tmp_path): - os.remove(tmp_path) - return - except Exception as e: - logger.error(f"Error downloading TikTok music: {str(e)}") - await status.edit_text(f"❌ Ошибка: {str(e)[:100]}") - if os.path.exists(tmp_path): - try: - os.remove(tmp_path) - except: - pass - return - finally: - ACTIVE_DOWNLOADS.pop(user_id, None) - - await status.edit_text("📤 Отправляю звук…", 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"✅ Готово! (Звук из TikTok)\n📦 Размер: {size_mb:.1f} МБ", - parse_mode="HTML" - ) - except Exception as e: - logger.error(f"Error sending audio: {e}") - await status.edit_text("❌ Ошибка при отправке звука") - - cleanup_tmp(TMP_DIR) - - -# ---------------- ORIGINAL QUALITY HANDLERS ---------------- - -@dp.callback_query(F.data == "original_quality") -async def handle_original_quality(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 - - if not check_rate_limit(user_id, RATE_LIMIT_SECONDS): - await callback.message.answer("⏳ Подожди немного перед следующим запросом") - return - - await callback.message.edit_reply_markup(reply_markup=None) - status = await callback.message.answer("🎬 Загрузка в оригинальном качестве…", parse_mode="HTML") - - key = cache_key(url, "original", audio=False) - final_path = cache_path(CACHE_DIR, key, "mp4") - tmp_path = os.path.join(TMP_DIR, f"{key}.mp4") - - # Проверяем кэш - if os.path.exists(final_path): - await status.edit_text("📤 Отправляю файл из кэша…", 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"✅ Готово! (Оригинальное качество)\n📦 Размер: {size_mb:.1f} МБ", - parse_mode="HTML" - ) - except Exception as e: - logger.error(f"Error sending cached file: {e}") - await status.edit_text("❌ Ошибка при отправке файла") - return - - cancel_event = threading.Event() - ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event} - loop = asyncio.get_running_loop() - progress_cb = make_progress_cb(loop, status) - - try: - await asyncio.to_thread( - download_original_quality, - url, - tmp_path, - COOKIES_FILE, - cancel_event, - progress_cb, - ) - - if cancel_event.is_set(): - if os.path.exists(tmp_path): - os.remove(tmp_path) - await status.edit_text("⛔ Загрузка отменена") - return - - os.rename(tmp_path, final_path) - - except DownloadCancelled: - await status.edit_text("⛔ Загрузка отменена") - if os.path.exists(tmp_path): - os.remove(tmp_path) - return - except Exception as e: - logger.error(f"Error downloading original quality: {e}") - await status.edit_text( - "❌ Не удалось скачать в оригинальном качестве\n" - "💡 Попробуй обычное качество" - ) - if os.path.exists(tmp_path): - os.remove(tmp_path) - return - finally: - ACTIVE_DOWNLOADS.pop(user_id, None) - - await status.edit_text("📤 Отправляю видео…", parse_mode="HTML") - - 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"✅ Готово! (Оригинальное качество)\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"✅ Отправлено как документ (Оригинальное качество)\n📦 Размер: {size_mb:.1f} МБ", - parse_mode="HTML" - ) - except Exception as e2: - logger.error(f"Error sending as document: {e2}") - await status.edit_text("❌ Ошибка при отправке файла") - if os.path.exists(final_path): - os.remove(final_path) - - cleanup_tmp(TMP_DIR) - - -# ---------------- STANDARD VIDEO HANDLER ---------------- +# ------------------------ YouTube/VK handlers ------------------------ @dp.callback_query(F.data.startswith("q:")) async def handle_video(callback: CallbackQuery): @@ -726,10 +318,6 @@ async def handle_video(callback: CallbackQuery): await callback.message.answer("❌ Ссылка не найдена") return - if not check_rate_limit(user_id, RATE_LIMIT_SECONDS): - await callback.message.answer("⏳ Подожди немного перед следующим запросом") - return - await callback.message.edit_reply_markup(reply_markup=None) status = await callback.message.answer("🔍 Анализирую ссылку…", parse_mode="HTML") @@ -768,55 +356,41 @@ async def handle_video(callback: CallbackQuery): cancel_event, progress_cb, ) - - # Проверяем, был ли отменен процесс + if cancel_event.is_set(): - if os.path.exists(tmp_path): - os.remove(tmp_path) + for p in [tmp_path, optimized_path]: + if os.path.exists(p): os.remove(p) await status.edit_text("⛔ Загрузка отменена") return - - # Оптимизируем видео для телеграма (кроме оригинального качества) + + # Оптимизируем видео для Telegram (кроме оригинального качества) if quality != "original": await status.edit_text("⚙️ Оптимизирую видео для телеграма…", parse_mode="HTML") await asyncio.to_thread(optimize_for_telegram, tmp_path, optimized_path) - - # Удаляем исходный файл и используем оптимизированный - if os.path.exists(tmp_path): - os.remove(tmp_path) - - os.rename(optimized_path, final_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) + except DownloadCancelled: + for p in [tmp_path, optimized_path]: + if os.path.exists(p): os.remove(p) await status.edit_text("⛔ Загрузка отменена") - for path in [tmp_path, optimized_path]: - if os.path.exists(path): - os.remove(path) return except Exception as e: logger.error(f"Error downloading video: {e}") - await status.edit_text( - "❌ Не удалось скачать видео\n" - "💡 Попробуй другое качество" - ) - for path in [tmp_path, optimized_path]: - if os.path.exists(path): - os.remove(path) + for p in [tmp_path, optimized_path]: + if os.path.exists(p): os.remove(p) + await status.edit_text("❌ Не удалось скачать видео") return finally: ACTIVE_DOWNLOADS.pop(user_id, None) - await status.edit_text("📤 Отправляю видео…", parse_mode="HTML") - 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_video(FSInputFile(final_path), supports_streaming=True) + size_mb = os.path.getsize(final_path) / (1024 * 1024) await callback.message.answer( f"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML" @@ -825,7 +399,7 @@ async def handle_video(callback: CallbackQuery): 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 + size_mb = os.path.getsize(final_path) / (1024 * 1024) await callback.message.answer( f"✅ Отправлено как документ\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML" @@ -833,13 +407,7 @@ async def handle_video(callback: CallbackQuery): except Exception as e2: logger.error(f"Error sending as document: {e2}") await status.edit_text("❌ Ошибка при отправке файла") - if os.path.exists(final_path): - os.remove(final_path) - cleanup_tmp(TMP_DIR) - - -# ---------------- STANDARD AUDIO HANDLER ---------------- @dp.callback_query(F.data == "audio") async def handle_audio(callback: CallbackQuery): @@ -851,25 +419,14 @@ async def handle_audio(callback: CallbackQuery): await callback.message.answer("❌ Ссылка не найдена") return - if not check_rate_limit(user_id, RATE_LIMIT_SECONDS): - await callback.message.answer("⏳ Подожди немного перед следующим запросом") - return - await callback.message.edit_reply_markup(reply_markup=None) status = await callback.message.answer("🎧 Подготовка аудио…", parse_mode="HTML") - # Получаем информацию о видео для метаданных - video_info = None - try: - video_info = await asyncio.to_thread(extract_info, url, COOKIES_FILE) - except: - pass - 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") - # Создаем директории если не существуют + # Создаем директории os.makedirs(TMP_DIR, exist_ok=True) os.makedirs(os.path.dirname(final_path), exist_ok=True) @@ -878,7 +435,7 @@ async def handle_audio(callback: CallbackQuery): await status.edit_text("📤 Отправляю аудио из кэша…", parse_mode="HTML") try: await callback.message.answer_audio(FSInputFile(final_path)) - size_mb = os.path.getsize(final_path) / 1024 / 1024 + size_mb = os.path.getsize(final_path) / (1024 * 1024) await callback.message.answer( f"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML" @@ -902,50 +459,50 @@ async def handle_audio(callback: CallbackQuery): cancel_event, progress_cb, ) - - # Проверяем, был ли отменен процесс + if cancel_event.is_set(): 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 - - # Проверяем, создан ли файл + if not os.path.exists(tmp_path): raise Exception("Аудио файл не был создан") - - # Проверяем размер файла + file_size = os.path.getsize(tmp_path) if file_size == 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 = get_video_info_for_audio(video_info) - await asyncio.to_thread(add_metadata_to_audio, tmp_path, tmp_path + "_meta.mp3", metadata) + 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) - - # Перемещаем файл в кэш - if os.path.exists(final_path): - os.remove(final_path) + os.rename(tmp_path, final_path) - + except DownloadCancelled: 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 except Exception as e: logger.error(f"Error downloading audio: {str(e)}") await status.edit_text(f"❌ Ошибка: {str(e)[:100]}") - if os.path.exists(tmp_path): - try: - os.remove(tmp_path) - except: - pass + if os.path.exists(tmp_path): os.remove(tmp_path) return finally: ACTIVE_DOWNLOADS.pop(user_id, None) @@ -954,7 +511,7 @@ async def handle_audio(callback: CallbackQuery): try: await callback.message.answer_audio(FSInputFile(final_path)) - size_mb = os.path.getsize(final_path) / 1024 / 1024 + size_mb = os.path.getsize(final_path) / (1024 * 1024) await callback.message.answer( f"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML" @@ -963,43 +520,35 @@ async def handle_audio(callback: CallbackQuery): logger.error(f"Error sending audio: {e}") await status.edit_text("❌ Ошибка при отправке аудио") - cleanup_tmp(TMP_DIR) - -# ---------------- 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 - - if not check_rate_limit(user_id, RATE_LIMIT_SECONDS * 3): - await callback.message.answer("⏳ Подожди немного перед следующим запросом") - return - + await callback.message.edit_reply_markup(reply_markup=None) status = await callback.message.answer("📁 Анализирую плейлист…", 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"⚠️ Внимание!\n\n" @@ -1011,9 +560,9 @@ 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("❌ Ошибка при анализе плейлиста") @@ -1023,28 +572,27 @@ async def handle_playlist_all(callback: CallbackQuery): 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): - """Загружает плейлист после подтверждения""" try: import uuid import shutil - + video_count = len(playlist_info['entries']) playlist_title = playlist_info.get('title', 'Плейлист') - + await status.edit_text( f"📁 Начинаю загрузку плейлиста\n\n" f"🎬 Название: {playlist_title}\n" @@ -1052,17 +600,15 @@ async def download_playlist_confirm(callback, user_id, playlist_info, status): f"⏳ Подготовка...", parse_mode="HTML" ) - + cancel_event = threading.Event() ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event} loop = asyncio.get_running_loop() - progress_cb = make_playlist_progress_cb(loop, status, video_count) - - # Создаем временную директорию для плейлиста + 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, @@ -1071,50 +617,42 @@ 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"📤 Отправляю {len(downloaded_files)} видео…", 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] - - # Отправляем файл с номером в подписи await callback.message.answer_document( FSInputFile(file_path), caption=f"🎬 Видео {i}/{len(downloaded_files)}\n📁 {display_name[:50]}" ) sent_count += 1 - - # Небольшая пауза между отправками await asyncio.sleep(1) - 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"✅ Плейлист загружен!\n\n" f"📁 Видео в плейлисте: {video_count}\n" @@ -1123,10 +661,9 @@ 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: @@ -1140,7 +677,6 @@ async def download_playlist_confirm(callback, user_id, playlist_info, status): @dp.callback_query(F.data == "playlist_confirm_no") async def handle_playlist_cancel(callback: CallbackQuery): await callback.answer("Отменено", show_alert=True) - await callback.message.edit_reply_markup(reply_markup=None) @dp.callback_query(F.data == "playlist_first") @@ -1148,57 +684,59 @@ 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( "🔽 Выбери формат загрузки для первого видео:", - reply_markup=quality_keyboard(), + reply_markup=youtube_quality_keyboard(), parse_mode="HTML" ) - + except Exception as e: logger.error(f"Error getting first video: {e}") await callback.message.answer("❌ Ошибка при получении видео из плейлиста") -# -------------------- entrypoint -------------------- +@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) + await callback.answer("⛔ Загрузка отменена", show_alert=True) + else: + await callback.answer("❌ Нет активной загрузки", show_alert=True) + + +# -------------------- ENTRYPOINT -------------- async def main(): # Очистка временных файлов при старте cleanup_tmp(TMP_DIR) - - # Запускаем задачу очистки кэша в фоне - cleanup_task = asyncio.create_task(scheduled_cache_cleanup()) - + try: await dp.start_polling(bot) - finally: - # Отменяем задачу очистки при выходе - cleanup_task.cancel() - try: - await cleanup_task - except asyncio.CancelledError: - pass + except KeyboardInterrupt: + logger.info("Bot stopped by user") + if __name__ == "__main__": try: asyncio.run(main()) - except KeyboardInterrupt: - logger.info("Bot stopped by user") except Exception as e: logger.error(f"Fatal error: {e}") \ No newline at end of file diff --git a/wireguard/wg_confs/wg0.conf b/wireguard/wg_confs/wg0.conf new file mode 100644 index 0000000..ee3c0b9 --- /dev/null +++ b/wireguard/wg_confs/wg0.conf @@ -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 \ No newline at end of file From 525d52cc767f77c523ed03934de414a6e01fbf37 Mon Sep 17 00:00:00 2001 From: smolkik-code Date: Wed, 18 Feb 2026 17:17:55 +0700 Subject: [PATCH 2/6] bug vix1 --- bot/cache.py | 16 +-- bot/downloader.py | 355 +++++++++++++++------------------------------- bot/main.py | 255 +++++++++------------------------ 3 files changed, 183 insertions(+), 443 deletions(-) diff --git a/bot/cache.py b/bot/cache.py index 285f673..0fbdc93 100644 --- a/bot/cache.py +++ b/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}") \ No newline at end of file diff --git a/bot/downloader.py b/bot/downloader.py index 52d9f46..7a9eead 100644 --- a/bot/downloader.py +++ b/bot/downloader.py @@ -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) \ No newline at end of file diff --git a/bot/main.py b/bot/main.py index 555a929..98dc2ca 100644 --- a/bot/main.py +++ b/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 = ( - "⏬ Загрузка\n" - f"{bar} {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"⏬ Загрузка\n{bar} {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("🎵 Загружаю TikTok (видео + аудио)...", 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("📤 Отправляю видео + аудио...", 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("📸 Загружаю Instagram (1080p)...", 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("📤 Отправляю видео...", 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( "👋 Привет!\n\n" - "📥 Я скачиваю видео и звук из видео по ссылке.\n\n" + "📥 Скачиваю видео и аудио по ссылке.\n\n" "✨ Новые возможности:\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("📤 Отправляю файл из кэша…", 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"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ", - parse_mode="HTML" - ) + size_mb = os.path.getsize(final_path) / (1024 * 1024) + await callback.message.answer(f"✅ Готово!\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("⚙️ Оптимизирую видео для телеграма…", 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"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ", - parse_mode="HTML" - ) + await callback.message.answer(f"✅ Готово!\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"✅ Отправлено как документ\n📦 Размер: {size_mb:.1f} МБ", - parse_mode="HTML" - ) + await callback.message.answer(f"✅ Отправлено как документ\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("📤 Отправляю аудио из кэша…", 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"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ", - parse_mode="HTML" - ) + await callback.message.answer(f"✅ Готово!\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("📤 Отправляю аудио…", 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"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ", - parse_mode="HTML" - ) + await callback.message.answer(f"✅ Готово!\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("📁 Анализирую плейлист…", 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"⚠️ Внимание!\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"📁 Начинаю загрузку плейлиста\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"📁 Начинаю загрузку плейлиста\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"📤 Отправляю {len(downloaded_files)} видео…", 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"✅ Плейлист загружен!\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()) From 817819c3825a93ed556c2fb092fe71d55fa2576c Mon Sep 17 00:00:00 2001 From: smolkik-code Date: Wed, 18 Feb 2026 18:58:25 +0700 Subject: [PATCH 3/6] bug vix2 --- bot/cache.py | 8 +++++++- bot/main.py | 18 ++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/bot/cache.py b/bot/cache.py index 0fbdc93..40b5772 100644 --- a/bot/cache.py +++ b/bot/cache.py @@ -1,7 +1,13 @@ # cache.py +import hashlib import os + +def cache_key(url: str, quality: str, audio: bool = False) -> str: + key_str = f"{url}_{quality}_{audio}" + return hashlib.sha256(key_str.encode()).hexdigest() + def cache_path(cache_dir: str, key: str, extension: str) -> str: subdir = key[:2] 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}") \ No newline at end of file diff --git a/bot/main.py b/bot/main.py index 98dc2ca..831b9fc 100644 --- a/bot/main.py +++ b/bot/main.py @@ -1,10 +1,9 @@ -# main.py +# main.py — ИСПРАВЛЕНО: добавлен import cache_key import os import asyncio import threading import logging import time -from datetime import datetime from typing import Optional from aiogram import Bot, Dispatcher, F @@ -33,7 +32,7 @@ from downloader import ( 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_path +from cache import cache_key, cache_path # ← ИСПРАВЛЕНО: добавлен cache_key from cleanup import cleanup_tmp logging.basicConfig(level=logging.INFO) @@ -56,7 +55,6 @@ USER_URLS: dict[int, str] = {} USER_DATA: dict[int, dict] = {} ACTIVE_DOWNLOADS: dict[int, dict] = {} -# Вспомогательные функции def render_bar(percent: float, size: int = 10) -> str: filled = int(size * percent / 100) return "█" * filled + "░" * (size - filled) @@ -89,7 +87,7 @@ def make_progress_cb(loop, message): asyncio.run_coroutine_threadsafe(update(d), loop) return cb -# ---------------------- AUTO DOWNLOADS (TikTok/Instagram) ---------------------- +# ---------------------- AUTO DOWNLOADS (TikTok/Instagram) ------------------ async def process_tiktok_auto(message: Message, user_id: int, url: str): status = await message.answer("🎵 Загружаю TikTok (видео + аудио)...", parse_mode="HTML") @@ -97,7 +95,6 @@ async def process_tiktok_auto(message: Message, user_id: int, url: str): 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") tmp_video = cache_path(TMP_DIR, key_video, "mp4") os.makedirs(os.path.dirname(tmp_video), exist_ok=True) @@ -224,7 +221,7 @@ 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): @@ -275,8 +272,6 @@ async def handle_link(message: Message): parse_mode="HTML" ) -# ---------------------- YOUTUBE/VK HANDLERS ---------------------- - @dp.callback_query(F.data.startswith("q:")) async def handle_video(callback: CallbackQuery): await callback.answer() @@ -329,7 +324,6 @@ async def handle_video(callback: CallbackQuery): await status.edit_text("⛔ Загрузка отменена") return - # ✅ ИСПРАВЛЕНО: просто перемещаем без оптимизации 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) @@ -439,7 +433,7 @@ async def handle_audio(callback: CallbackQuery): 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() @@ -602,7 +596,7 @@ async def cancel_download(callback: CallbackQuery): else: await callback.answer("❌ Нет активной загрузки", show_alert=True) -# ---------------------- ENTRYPOINT ---------------------- +# ---------------------- ENTRYPOINT ------------------ async def main(): cleanup_tmp(TMP_DIR) try: From 5b0152e47cc2f2b9104a50add38e904441b6af7a Mon Sep 17 00:00:00 2001 From: smolkik-code Date: Wed, 18 Feb 2026 22:34:53 +0700 Subject: [PATCH 4/6] bug vix3 --- bot/downloader.py | 282 +++++++++++++++++++++++++++++++--------------- bot/main.py | 47 ++------ 2 files changed, 205 insertions(+), 124 deletions(-) diff --git a/bot/downloader.py b/bot/downloader.py index 7a9eead..3a2f3e2 100644 --- a/bot/downloader.py +++ b/bot/downloader.py @@ -3,8 +3,8 @@ import yt_dlp import threading import os import logging -import shutil import subprocess +import shutil from typing import List from config import ( @@ -26,7 +26,134 @@ def _progress_hook(cancel_event, progress_cb): progress_cb(d) 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( url: str, video_path: str, @@ -35,15 +162,14 @@ def download_tiktok_video_and_audio( cancel_event: threading.Event, progress_cb, ): - # 1. Скачиваем видео (1080p) — УБРАН skip_impersonation + # 1. Скачиваем видео video_opts = { "format": f"bestvideo[height<={DEFAULT_TIKTOK_VIDEO_QUALITY}]+bestaudio/best", - "outtmpl": video_path.replace('.mp4', ''), - "merge_output_format": "mp4", + "outtmpl": video_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": False, # ← ВКЛЮЧИЛ предупреждения для отладки + "no_warnings": False, "concurrent_fragment_downloads": 4, "http_chunk_size": 10485760, } @@ -51,10 +177,24 @@ def download_tiktok_video_and_audio( with yt_dlp.YoutubeDL(video_opts) as ydl: 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") - # 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 = { "format": "bestaudio/best", "outtmpl": audio_path.replace('.mp3', ''), @@ -65,7 +205,7 @@ def download_tiktok_video_and_audio( "preferredquality": DEFAULT_TIKTOK_AUDIO_QUALITY, }], "quiet": True, - "no_warnings": False, # ← ВКЛЮЧИЛ для отладки + "no_warnings": False, "concurrent_fragment_downloads": 4, "http_chunk_size": 10485760, } @@ -73,10 +213,27 @@ def download_tiktok_video_and_audio( with yt_dlp.YoutubeDL(audio_opts) as ydl: 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") -# ---------------- 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( url: str, out_path: str, @@ -84,68 +241,37 @@ def download_instagram_video( 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, - out_path: str, - cookies: str | None, - cancel_event: threading.Event, - progress_cb, -): - ydl_opts = { - "format": f"best[height<={quality}]/best", "outtmpl": out_path, - "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": True, + "no_warnings": False, "concurrent_fragment_downloads": 4, "http_chunk_size": 10485760, } + with yt_dlp.YoutubeDL(ydl_opts) as ydl: 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( playlist_info: dict, output_dir: str, @@ -157,7 +283,6 @@ def download_playlist_videos( 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 if cookies and os.path.exists(cookies) else None, "progress_hooks": [_progress_hook(cancel_event, progress_cb)], "quiet": True, @@ -168,45 +293,24 @@ 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 = 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") continue try: 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)) + if info: + filename = ydl.prepare_filename(info) + if os.path.exists(filename): + downloaded_files.append(filename) except Exception as e: logger.error(f"Error downloading video {i}: {e}") - continue return downloaded_files except Exception as e: logger.error(f"Playlist download error: {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 = [] - 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) \ No newline at end of file + raise \ No newline at end of file diff --git a/bot/main.py b/bot/main.py index 831b9fc..d48115a 100644 --- a/bot/main.py +++ b/bot/main.py @@ -1,4 +1,4 @@ -# main.py — ИСПРАВЛЕНО: добавлен import cache_key +# main.py import os import asyncio import threading @@ -17,8 +17,6 @@ from config import ( CACHE_DIR, TMP_DIR, COOKIES_FILE, - CACHE_MAX_AGE_DAYS, - CACHE_MAX_SIZE_MB, ) from keyboards import youtube_quality_keyboard, cancel_keyboard, playlist_keyboard from downloader import ( @@ -27,18 +25,17 @@ from downloader import ( download_video, download_audio, download_playlist_videos, + optimize_for_telegram, 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 # ← ИСПРАВЛЕНО: добавлен cache_key +from cache import cache_key, cache_path from cleanup import cleanup_tmp logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -# Инициализация if LOCAL_API_URL: api_server = TelegramAPIServer.from_base(LOCAL_API_URL) session = AiohttpSession(api=api_server) @@ -87,8 +84,7 @@ def make_progress_cb(loop, message): asyncio.run_coroutine_threadsafe(update(d), loop) return cb -# ---------------------- AUTO DOWNLOADS (TikTok/Instagram) ------------------ - +# ---------------- TikTok + Instagram (автозагрузка) ---------------- async def process_tiktok_auto(message: Message, user_id: int, url: str): status = await message.answer("🎵 Загружаю TikTok (видео + аудио)...", 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") os.makedirs(os.path.dirname(tmp_audio), exist_ok=True) - # Проверка кэша if os.path.exists(video_cache) and os.path.exists(audio_cache): await status.edit_text("📤 Отправляю из кэша...", parse_mode="HTML") try: @@ -118,7 +113,6 @@ async def process_tiktok_auto(message: Message, user_id: int, url: str): cleanup_tmp(TMP_DIR) return - # Загрузка cancel_event = threading.Event() ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event} loop = asyncio.get_running_loop() @@ -134,19 +128,16 @@ async def process_tiktok_auto(message: Message, user_id: int, url: str): cancel_event, progress_cb, ) - if cancel_event.is_set(): await status.edit_text("⛔ Загрузка отменена") for p in [tmp_video, tmp_audio]: if os.path.exists(p): os.remove(p) return - # Перемещение в кэш - 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) + # Уже готовы Telegram-safe mp4/m3, просто перемещаем + if os.path.exists(video_cache): os.remove(video_cache) os.rename(tmp_video, video_cache) + if os.path.exists(audio_cache): os.remove(audio_cache) os.rename(tmp_audio, audio_cache) await status.edit_text("📤 Отправляю видео + аудио...", 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") os.makedirs(os.path.dirname(tmp_path), exist_ok=True) - # Проверка кэша if os.path.exists(final_cache): await status.edit_text("📤 Отправляю из кэша...", parse_mode="HTML") try: @@ -198,15 +188,12 @@ async def process_instagram_auto(message: Message, user_id: int, url: str): cancel_event, progress_cb, ) - if cancel_event.is_set(): await status.edit_text("⛔ Загрузка отменена") if os.path.exists(tmp_path): os.remove(tmp_path) return - # Перемещение в кэш 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("📤 Отправляю видео...", 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) cleanup_tmp(TMP_DIR) -# ---------------------- HANDLERS ------------------ - +# ---------------- Handlers ---------------- @dp.message(F.text == "/start") async def start(message: Message): await message.answer( "👋 Привет!\n\n" "📥 Скачиваю видео и аудио по ссылке.\n\n" "✨ Новые возможности:\n" - "• 🎬 TikTok: автоматически видео + аудио\n" - "• 📸 Instagram: автоматически видео в 1080p\n" + "• 🎬 TikTok: автоматически видео + аудио (Telegram-safe)\n" + "• 📸 Instagram: автоматически видео в 1080p (Telegram-safe)\n" "• 🎵 Аудио из любого видео\n" "• 📁 Плейлисты YouTube\n" "👉 Просто отправь ссылку.", @@ -245,17 +231,14 @@ async def handle_link(message: Message): platform_info = await asyncio.to_thread(get_platform_info, url) - # TikTok — автоматически if platform_info == "tiktok": await process_tiktok_auto(message, user_id, url) return - # Instagram — автоматически if platform_info == "instagram": await process_instagram_auto(message, user_id, url) return - # Плейлисты if await asyncio.to_thread(is_playlist, url): await message.answer( "📁 Обнаружен плейлист!\n\n" @@ -265,7 +248,6 @@ async def handle_link(message: Message): ) return - # YouTube/VK — выбор качества await message.answer( "🔽 Выбери качество:", reply_markup=youtube_quality_keyboard(), @@ -291,7 +273,6 @@ async def handle_video(callback: CallbackQuery): 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("📤 Отправляю файл из кэша…", parse_mode="HTML") try: @@ -318,14 +299,12 @@ async def handle_video(callback: CallbackQuery): cancel_event, progress_cb, ) - if cancel_event.is_set(): if os.path.exists(tmp_path): os.remove(tmp_path) await status.edit_text("⛔ Загрузка отменена") return 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: @@ -372,7 +351,6 @@ async def handle_audio(callback: CallbackQuery): tmp_path = cache_path(TMP_DIR, key, "mp3") os.makedirs(os.path.dirname(tmp_path), exist_ok=True) - # Проверка кэша if os.path.exists(final_path): await status.edit_text("📤 Отправляю аудио из кэша…", parse_mode="HTML") try: @@ -398,7 +376,6 @@ async def handle_audio(callback: CallbackQuery): cancel_event, progress_cb, ) - if cancel_event.is_set(): await status.edit_text("⛔ Загрузка отменена") if os.path.exists(tmp_path): os.remove(tmp_path) @@ -410,6 +387,7 @@ async def handle_audio(callback: CallbackQuery): os.remove(tmp_path) raise Exception("Создан пустой аудио файл") + if os.path.exists(final_path): os.remove(final_path) os.rename(tmp_path, final_path) except DownloadCancelled: @@ -433,7 +411,7 @@ async def handle_audio(callback: CallbackQuery): 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() @@ -596,7 +574,6 @@ async def cancel_download(callback: CallbackQuery): else: await callback.answer("❌ Нет активной загрузки", show_alert=True) -# ---------------------- ENTRYPOINT ------------------ async def main(): cleanup_tmp(TMP_DIR) try: From 089e90b36adf57bdee921c67522c368c184e9ec2 Mon Sep 17 00:00:00 2001 From: smolkik-code Date: Wed, 18 Feb 2026 23:30:11 +0700 Subject: [PATCH 5/6] bug vix4 --- .DS_Store | Bin 16388 -> 16388 bytes bot/downloader.py | 68 +++++++++++++++++++++++++--------------------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/.DS_Store b/.DS_Store index 46d352977d5e59d2ac7753f36f05111047a7ce12..db8bed2fe30eb4d47ef8fdf0a5153583ed2296a4 100644 GIT binary patch delta 155 zcmZo^U~Fk%+^|-jTcWz!(!^Lt!Pwk*@@{$Q$?K(6c$pcJ81flPa*E5GlX5n*D44TP g4%aJVM^m=>mpU)UWL+Ef&D;iy*oiU7VkHsE0M;%n-T(jq delta 274 zcmZo^U~Fk%+^|+&OS-z+(A>yGM?u%nz`&wbN1@u%$UsNI(!j8`mXkwNS>HM+K07Bj zFTZ#425EV|J_ZH`W`-n&e1?*o;H-{-4QyC8a~mvT OCs8|wJ1kaGXaoS)lSz31 diff --git a/bot/downloader.py b/bot/downloader.py index 3a2f3e2..f08127a 100644 --- a/bot/downloader.py +++ b/bot/downloader.py @@ -5,6 +5,7 @@ import os import logging import subprocess import shutil +import asyncio from typing import List from config import ( @@ -28,11 +29,7 @@ def _progress_hook(cancel_event, progress_cb): def optimize_for_telegram(input_path: str, output_path: str) -> None: """ - Конвертирует видео в Telegram-safe формат: - - mp4 + h264 + aac - - четные размеры - - crf: 23 (или 28 >50MB) - - faststart + Синхронная функция: конвертирует видео в Telegram-safe mp4 """ file_size_mb = os.path.getsize(input_path) / (1024 * 1024) crf = 28 if file_size_mb > 50 else 23 @@ -66,10 +63,9 @@ def download_video( cancel_event: threading.Event, progress_cb, ): - # 1. Скачиваем в любой формат ydl_opts = { "format": f"best[height<={quality}]/best", - "outtmpl": out_path, # ← без .mp4! + "outtmpl": out_path, "cookiefile": cookies if cookies and os.path.exists(cookies) else None, "progress_hooks": [_progress_hook(cancel_event, progress_cb)], "quiet": True, @@ -81,7 +77,7 @@ def download_video( 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']: @@ -93,11 +89,11 @@ def download_video( if not actual_video: raise Exception(f"Video file not created for {url}") - # 3. Конвертируем в Telegram-safe mp4 + # Конвертируем в Telegram-safe mp4 (синхронно) optimized_path = base + "_telegram.mp4" - await asyncio.to_thread(optimize_for_telegram, actual_video, optimized_path) + optimize_for_telegram(actual_video, optimized_path) # ← без await! - # 4. Удаляем оригинальный файл и ставим конвертированный + # Удаляем старый файл и ставим конвертированный for path in [out_path, actual_video]: if os.path.exists(path): os.remove(path) os.rename(optimized_path, out_path) @@ -113,7 +109,7 @@ def download_audio( ): ydl_opts = { "format": "bestaudio/best", - "outtmpl": out_path.replace('.mp3', ''), # ← без .mp3! + "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": [{ @@ -130,7 +126,7 @@ def download_audio( 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']: @@ -142,18 +138,15 @@ def download_audio( if not actual_audio: raise Exception(f"Audio file not created for {url}") - # Конвертируем в mp3 (если нужно) + # Конвертируем в 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 - ]) + optimize_for_telegram_audio(actual_audio, optimized) # ← отдельная функция if os.path.exists(out_path): os.remove(out_path) os.rename(optimized, out_path) -# ---------------- TIKTOK: Video + Audio (автоматически) ------ +# ---------------- TikTok: Video + Audio (автоматически) ------ def download_tiktok_video_and_audio( url: str, video_path: str, @@ -162,10 +155,10 @@ def download_tiktok_video_and_audio( cancel_event: threading.Event, progress_cb, ): - # 1. Скачиваем видео + # Скачиваем видео video_opts = { "format": f"bestvideo[height<={DEFAULT_TIKTOK_VIDEO_QUALITY}]+bestaudio/best", - "outtmpl": video_path, # ← без .mp4! + "outtmpl": video_path, "cookiefile": cookies if cookies and os.path.exists(cookies) else None, "progress_hooks": [_progress_hook(cancel_event, progress_cb)], "quiet": True, @@ -177,6 +170,7 @@ def download_tiktok_video_and_audio( with yt_dlp.YoutubeDL(video_opts) as ydl: ydl.download([url]) + # Ищем видеофайл base_video = video_path.rsplit('.', 1)[0] actual_video = None for ext in ['', '.mp4', '.mov', '.webm']: @@ -187,14 +181,14 @@ def download_tiktok_video_and_audio( if not actual_video: raise Exception("TikTok video file not created") - # Конвертируем в Telegram-safe + # Конвертируем в Telegram-safe mp4 optimized_video = base_video + "_telegram.mp4" - await asyncio.to_thread(optimize_for_telegram, actual_video, optimized_video) + 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 = { "format": "bestaudio/best", "outtmpl": audio_path.replace('.mp3', ''), @@ -225,15 +219,12 @@ def download_tiktok_video_and_audio( 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 - ]) + optimize_for_telegram_audio(actual_audio, optimized_audio) if os.path.exists(audio_path): os.remove(audio_path) os.rename(optimized_audio, audio_path) -# ---------------- INSTAGRAM: Video (1080p) ------ +# ---------------- Instagram: Video (1080p) ------ def download_instagram_video( url: str, out_path: str, @@ -266,12 +257,27 @@ def download_instagram_video( raise Exception("Instagram video not created") optimized = base + "_telegram.mp4" - await asyncio.to_thread(optimize_for_telegram, actual, optimized) + optimize_for_telegram(actual, optimized) if os.path.exists(out_path): os.remove(out_path) os.rename(optimized, out_path) -# ---------------- PLAYLIST ------ +# ---------------- Audio helper (синхронная) ------ +def optimize_for_telegram_audio(input_path: str, output_path: str) -> None: + """Конвертирует аудио в Telegram-safe mp3""" + cmd = [ + 'ffmpeg', + '-i', input_path, + '-c:a', 'aac', + '-b:a', '192k', + '-loglevel', 'error', + '-y', + output_path + ] + subprocess.run(cmd, capture_output=True, text=True, timeout=600) + + +# ---------------- Playlist ------ def download_playlist_videos( playlist_info: dict, output_dir: str, From c49e080d411b833632bb93e7a8162b73d95ef96d Mon Sep 17 00:00:00 2001 From: smolkik-code Date: Thu, 19 Feb 2026 10:15:04 +0700 Subject: [PATCH 6/6] Resolve merge conflicts --- CHANGELOG.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29