diff --git a/.DS_Store b/.DS_Store
index db8bed2..5897e57 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/bot/cleanup.py b/bot/cleanup.py
index 2ca9daf..f64cf87 100644
--- a/bot/cleanup.py
+++ b/bot/cleanup.py
@@ -1,9 +1,53 @@
+# cleanup.py
import os
import time
-def cleanup_tmp(path, max_age=3600):
+
+def get_size_in_mb(path: str) -> float:
+ total_size = 0
+ for dirpath, _, filenames in os.walk(path):
+ for filename in filenames:
+ filepath = os.path.join(dirpath, filename)
+ if os.path.exists(filepath):
+ total_size += os.path.getsize(filepath)
+ return total_size / (1024 ** 2)
+
+
+def clear_old_files(directory: str, max_age_seconds: int = 3600):
now = time.time()
- for f in os.listdir(path):
- p = os.path.join(path, f)
- if os.path.isfile(p) and now - os.path.getmtime(p) > max_age:
- os.remove(p)
+ for root, _, files in os.walk(directory):
+ for file in files:
+ file_path = os.path.join(root, file)
+ try:
+ if now - os.path.getmtime(file_path) > max_age_seconds:
+ os.remove(file_path)
+ except Exception as e:
+ pass
+
+
+def ensure_cache_size(limit_mb: float, cleanup_directory: str):
+ while get_size_in_mb(cleanup_directory) > limit_mb:
+ oldest_mtime = float('inf')
+ oldest_file = None
+ for root, _, files in os.walk(cleanup_directory):
+ for file in files:
+ file_path = os.path.join(root, file)
+ try:
+ mtime = os.path.getmtime(file_path)
+ if mtime < oldest_mtime:
+ oldest_mtime = mtime
+ oldest_file = file_path
+ except Exception:
+ continue
+
+ if oldest_file:
+ try:
+ os.remove(oldest_file)
+ except Exception:
+ break
+ else:
+ break
+
+
+def cleanup_tmp(path: str, max_age: int = 3600):
+ clear_old_files(path, max_age)
diff --git a/bot/config.py b/bot/config.py
index e1719ae..a70dd02 100644
--- a/bot/config.py
+++ b/bot/config.py
@@ -1,40 +1,41 @@
# config.py
import os
from dotenv import load_dotenv
+import logging
load_dotenv()
# Telegram
BOT_TOKEN = os.getenv("BOT_TOKEN")
-LOCAL_API_URL = os.getenv("LOCAL_API_URL")
+if not BOT_TOKEN:
+ raise RuntimeError("Не задан BOT_TOKEN в переменных окружения")
-# Paths
+LOCAL_API_URL = os.getenv("LOCAL_API_URL")
DOWNLOAD_DIR = "/downloads"
CACHE_DIR = "/downloads/cache"
TMP_DIR = "/downloads/tmp"
-# yt-dlp
COOKIES_FILE = os.getenv("COOKIES_FILE")
+if COOKIES_FILE and not os.path.exists(COOKIES_FILE):
+ print(f"Указанный файл кукисов {COOKIES_FILE} не найден")
-# Limits
MAX_DURATION_SECONDS = int(os.getenv("MAX_DURATION_SECONDS", 1800))
RATE_LIMIT_SECONDS = int(os.getenv("RATE_LIMIT_SECONDS", 20))
MAX_FILE_SIZE_MB = 2000
CACHE_MAX_AGE_DAYS = 7
CACHE_MAX_SIZE_MB = 4096
-# Авто-качества для TikTok и Instagram (без выбора)
-DEFAULT_TIKTOK_VIDEO_QUALITY = "1080"
-DEFAULT_TIKTOK_AUDIO_QUALITY = "192" # кбит/с
-DEFAULT_INSTAGRAM_VIDEO_QUALITY = "1080"
+DEFAULT_TIKTOK_VIDEO_QUALITY = int(os.getenv("DEFAULT_TIKTOK_VIDEO_QUALITY", "1080"))
+DEFAULT_TIKTOK_AUDIO_QUALITY = int(os.getenv("DEFAULT_TIKTOK_AUDIO_QUALITY", "192"))
+DEFAULT_INSTAGRAM_VIDEO_QUALITY = int(os.getenv("DEFAULT_INSTAGRAM_VIDEO_QUALITY", "1080"))
-# Private mode
ALLOWED_USERS = set(
int(uid.strip())
for uid in os.getenv("ALLOWED_USERS", "").split(",")
if uid.strip()
)
-# Ensure directories exist
os.makedirs(CACHE_DIR, exist_ok=True)
-os.makedirs(TMP_DIR, exist_ok=True)
\ No newline at end of file
+os.makedirs(TMP_DIR, exist_ok=True)
+
+logging.basicConfig(level=logging.INFO)
diff --git a/bot/downloader.py b/bot/downloader.py
index f08127a..5727940 100644
--- a/bot/downloader.py
+++ b/bot/downloader.py
@@ -6,7 +6,7 @@ import logging
import subprocess
import shutil
import asyncio
-from typing import List
+from typing import List, Optional, Callable
from config import (
DEFAULT_TIKTOK_VIDEO_QUALITY,
@@ -27,11 +27,12 @@ def _progress_hook(cancel_event, progress_cb):
progress_cb(d)
return hook
+
def optimize_for_telegram(input_path: str, output_path: str) -> None:
"""
- Синхронная функция: конвертирует видео в Telegram-safe mp4
+ Асинхронная функция: конвертирует видео в Telegram-safe mp4
"""
- file_size_mb = os.path.getsize(input_path) / (1024 * 1024)
+ file_size_mb = os.path.getsize(input_path) / (1024 ** 2)
crf = 28 if file_size_mb > 50 else 23
cmd = [
@@ -48,275 +49,69 @@ def optimize_for_telegram(input_path: str, output_path: str) -> None:
output_path
]
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
- if result.returncode != 0:
- logger.error(f"FFmpeg error: {result.stderr}")
+ process = await asyncio.create_subprocess_exec(
+ *cmd,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE
+ )
+
+ stdout, stderr = await process.communicate()
+
+ if process.returncode != 0:
+ logger.error(f"FFmpeg error: {stderr.decode()}")
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,
-):
- ydl_opts = {
- "format": f"best[height<={quality}]/best",
- "outtmpl": out_path,
- "cookiefile": cookies if cookies and os.path.exists(cookies) else None,
- "progress_hooks": [_progress_hook(cancel_event, progress_cb)],
+async def process_download(message, url: str, output_path: str, cookies_file: Optional[str] = None, cancel_event: threading.Event = None):
+ ydl_options = {
+ "format": f"bestvideo[height<={DEFAULT_INSTAGRAM_VIDEO_QUALITY}] + bestaudio/best",
+ "outtmpl": output_path,
+ "cookiefile": cookies_file if cookies_file and os.path.exists(cookies_file) else None,
+ "progress_hooks": [lambda d: asyncio.create_task(progress_callback(message, d))],
"quiet": True,
"no_warnings": True,
"concurrent_fragment_downloads": 4,
- "http_chunk_size": 10485760,
+ "http_chunk_size": 1024 ** 2,
}
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
- ydl.download([url])
+ async def progress_callback(msg, d):
+ if d["status"] == "downloading":
+ downloaded_bytes = d.get("downloaded_bytes", 0)
+ total_bytes = d.get("total_bytes_est", None)
- # Ищем реальный файл
- 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 total_bytes:
+ percent_downloaded = (downloaded_bytes / total_bytes) * 100
+ download_speed_bps = d.get("speed", 1e-6)
+ estimated_remaining_time_seconds = max(0, (total_bytes - downloaded_bytes) / download_speed_bps)
- if not actual_video:
- raise Exception(f"Video file not created for {url}")
+ progress_text = "Downloading...\n"
+ progress_text += f"Progress: {percent_downloaded:.2f}%\n"
+ progress_text += f"Downloaded: {downloaded_bytes / (1024 ** 2):.2f} MB\n"
+ progress_text += f"Total Size: {total_bytes / (1024 ** 2):.2f} MB\n"
+ progress_text += f"[{d.get('filename')}] @ {int(download_speed_bps) / (1024 ** 2)} MB/s\n"
+ progress_text += f"ETA: {estimated_remaining_time_seconds:.1f} сек."
+ else:
+ progress_text = "Downloading...\n"
+ progress_text += f"Downloaded Bytes: {downloaded_bytes}\n"
- # Конвертируем в Telegram-safe mp4 (синхронно)
- optimized_path = base + "_telegram.mp4"
- optimize_for_telegram(actual_video, optimized_path) # ← без await!
-
- # Удаляем старый файл и ставим конвертированный
- 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', ''),
- "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])
-
- # Ищем файл
- 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"
- 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 (автоматически) ------
-def download_tiktok_video_and_audio(
- url: str,
- video_path: str,
- audio_path: str,
- cookies: str | None,
- cancel_event: threading.Event,
- progress_cb,
-):
- # Скачиваем видео
- video_opts = {
- "format": f"bestvideo[height<={DEFAULT_TIKTOK_VIDEO_QUALITY}]+bestaudio/best",
- "outtmpl": video_path,
- "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(video_opts) as ydl:
- ydl.download([url])
-
- # Ищем видеофайл
- 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")
-
- # Конвертируем в Telegram-safe mp4
- optimized_video = base_video + "_telegram.mp4"
- 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)
-
- # Аудио
- 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])
-
- 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")
-
- if actual_audio != audio_path:
- optimized_audio = base_audio + "_telegram.mp3"
- 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) ------
-def download_instagram_video(
- url: str,
- out_path: str,
- cookies: str | None,
- cancel_event: threading.Event,
- progress_cb,
-):
- ydl_opts = {
- "format": f"bestvideo[height<={DEFAULT_INSTAGRAM_VIDEO_QUALITY}]+bestaudio/best",
- "outtmpl": out_path,
- "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])
-
- 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"
- optimize_for_telegram(actual, optimized)
- if os.path.exists(out_path): os.remove(out_path)
- os.rename(optimized, out_path)
-
-
-# ---------------- 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,
- 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"),
- "cookiefile": cookies if cookies and os.path.exists(cookies) else None,
- "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:
+ await msg.edit_text(progress_text, parse_mode="HTML")
+ except Exception as e:
+ logger.error(f"Error updating progress message: {e}")
try:
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
- entries = playlist_info.get('entries', [])
- for i, entry in enumerate(entries, 1):
- if cancel_event.is_set():
- raise DownloadCancelled("Cancelled by user")
- if not entry.get('url'):
- continue
- try:
- info = ydl.extract_info(entry['url'], download=True)
- 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}")
- return downloaded_files
+ with yt_dlp.YoutubeDL(ydl_options) as ydl:
+ ydl.download([url])
+
+ await optimize_for_telegram(input_path=output_path, output_path=output_path + "_optimized")
except Exception as e:
- logger.error(f"Playlist download error: {e}")
+ logger.error(f"Download or optimization error: {e}")
+ raise
+
+async def download_ytdlp(url: str, ydl_options: dict):
+ try:
+ with yt_dlp.YoutubeDL(ydl_options) as ydl:
+ info = ydl.extract_info(url, download=True)
+ return info
+ except Exception as e:
+ logger.error(f"Error downloading from {url}: {e}")
raise
\ No newline at end of file