videoDownloadTGbot/bot.py

895 lines
42 KiB
Python
Raw Normal View History

import os
import re
import json
import logging
import asyncio
import sqlite3
import time
import subprocess
from pathlib import Path
from urllib.parse import urlparse
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor
import yt_dlp
import httpx
from telegram import Update
from telegram.ext import Application, MessageHandler, filters, ContextTypes, CommandHandler
# Настройка логирования
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
# Токен бота и имя бота из переменных окружения
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
TELEGRAM_BOT_USERNAME = os.getenv('TELEGRAM_BOT_USERNAME', 'vrubelVideoDownload_bot')
# URL VK сервиса для скачивания видео
VK_DOWNLOADER_URL = os.getenv('VK_DOWNLOADER_URL', 'http://localhost:5555')
# Базовая директория проекта (абсолютный путь), чтобы не зависеть от рабочей директории процесса
BASE_DIR = Path(__file__).resolve().parent
# Директория для временных файлов
DOWNLOADS_DIR = BASE_DIR / 'video'
DOWNLOADS_DIR.mkdir(parents=True, exist_ok=True)
# База данных (внутри папки data)
DATA_DIR = BASE_DIR / 'data'
DATA_DIR.mkdir(parents=True, exist_ok=True)
DB_FILE = DATA_DIR / 'bot.db'
# ThreadPoolExecutor для выполнения блокирующих операций (скачивание видео)
# Позволяет обрабатывать несколько запросов параллельно
DOWNLOAD_EXECUTOR = ThreadPoolExecutor(max_workers=5, thread_name_prefix="download")
def init_database():
"""Инициализирует базу данных и создает таблицы если их нет"""
try:
conn = sqlite3.connect(str(DB_FILE))
cursor = conn.cursor()
# Таблица пользователей
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
chat_id INTEGER PRIMARY KEY,
username TEXT,
first_name TEXT,
first_seen TEXT NOT NULL,
last_seen TEXT NOT NULL
)
''')
# Таблица статистики
cursor.execute('''
CREATE TABLE IF NOT EXISTS stats (
id INTEGER PRIMARY KEY CHECK (id = 1),
total_downloads INTEGER DEFAULT 0
)
''')
# Инициализируем stats если его нет
cursor.execute('SELECT COUNT(*) FROM stats')
if cursor.fetchone()[0] == 0:
cursor.execute('INSERT INTO stats (id, total_downloads) VALUES (1, 0)')
conn.commit()
conn.close()
logger.info("База данных инициализирована")
except Exception as e:
logger.error(f"Ошибка при инициализации базы данных: {e}")
def get_total_downloads() -> int:
"""Возвращает общее количество скачанных видео"""
try:
conn = sqlite3.connect(str(DB_FILE))
cursor = conn.cursor()
cursor.execute('SELECT total_downloads FROM stats WHERE id = 1')
result = cursor.fetchone()
conn.close()
return result[0] if result else 0
except Exception as e:
logger.error(f"Ошибка при получении количества скачанных видео: {e}")
return 0
def increment_downloads():
"""Увеличивает счетчик скачанных видео"""
try:
conn = sqlite3.connect(str(DB_FILE))
cursor = conn.cursor()
cursor.execute('UPDATE stats SET total_downloads = total_downloads + 1 WHERE id = 1')
conn.commit()
new_total = get_total_downloads()
conn.close()
logger.info(f"Общее количество скачанных видео: {new_total}")
except Exception as e:
logger.error(f"Ошибка при увеличении счетчика скачанных видео: {e}")
def get_total_users() -> int:
"""Возвращает общее количество уникальных пользователей"""
try:
conn = sqlite3.connect(str(DB_FILE))
cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) FROM users')
result = cursor.fetchone()
conn.close()
return result[0] if result else 0
except Exception as e:
logger.error(f"Ошибка при получении количества пользователей: {e}")
return 0
def add_user(chat_id: int, username: str = None, first_name: str = None):
"""Добавляет пользователя в базу данных или обновляет информацию о нем"""
try:
now = datetime.now().isoformat()
conn = sqlite3.connect(str(DB_FILE))
cursor = conn.cursor()
# Проверяем, существует ли пользователь
cursor.execute('SELECT chat_id FROM users WHERE chat_id = ?', (chat_id,))
exists = cursor.fetchone()
if exists:
# Обновляем last_seen
cursor.execute(
'UPDATE users SET last_seen = ?, username = ?, first_name = ? WHERE chat_id = ?',
(now, username, first_name, chat_id)
)
else:
# Добавляем нового пользователя
cursor.execute(
'INSERT INTO users (chat_id, username, first_name, first_seen, last_seen) VALUES (?, ?, ?, ?, ?)',
(chat_id, username, first_name, now, now)
)
total_users = get_total_users()
logger.info(f"Добавлен новый пользователь (chat_id: {chat_id}). Всего пользователей: {total_users}")
conn.commit()
conn.close()
except Exception as e:
logger.error(f"Ошибка при добавлении пользователя: {e}")
def detect_video_source(url: str) -> str:
"""Определяет источник видео по URL"""
domain = urlparse(url).netloc.lower()
if 'youtube.com' in domain or 'youtu.be' in domain:
return 'youtube'
elif 'instagram.com' in domain:
return 'instagram'
elif 'vk.com' in domain or 'vkontakte.ru' in domain:
return 'vk'
else:
return 'unknown'
def extract_urls_from_text(text: str) -> list[str]:
"""Извлекает все URL из текста сообщения"""
# Регулярное выражение для поиска URL (http/https)
url_pattern = r'https?://[^\s<>"{}|\\^`\[\]]+'
urls = re.findall(url_pattern, text)
return urls
def cleanup_old_files(max_age_hours: int = 24):
"""Удаляет старые файлы и .part файлы из папки загрузок"""
try:
current_time = time.time()
max_age_seconds = max_age_hours * 3600
for file_path in DOWNLOADS_DIR.glob('*'):
if not file_path.is_file():
continue
# Удаляем все .part файлы (недокачанные)
if file_path.suffix == '.part':
try:
file_path.unlink()
logger.info(f"Удален .part файл: {file_path.name}")
except Exception as e:
logger.warning(f"Не удалось удалить .part файл {file_path.name}: {e}")
continue
# Удаляем старые файлы (старше max_age_hours)
try:
file_age = current_time - file_path.stat().st_mtime
if file_age > max_age_seconds:
file_path.unlink()
logger.info(f"Удален старый файл: {file_path.name} (возраст: {file_age/3600:.1f} часов)")
except Exception as e:
logger.warning(f"Не удалось проверить/удалить файл {file_path.name}: {e}")
except Exception as e:
logger.error(f"Ошибка при очистке старых файлов: {e}")
def _safe_filename(title: str, chat_id: int) -> str:
"""Создает безопасное имя файла"""
safe_title = re.sub(r'[<>:"/\\|?*]', '', title)[:100]
return str(DOWNLOADS_DIR / f'{chat_id}_{safe_title}.%(ext)s')
async def download_youtube_video(url: str, chat_id: int, max_retries: int = 3) -> str:
"""Скачивает видео с YouTube"""
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
last_error = None
for attempt in range(max_retries):
try:
# Получаем информацию о видео
ydl_opts_info = {
'quiet': False,
'no_warnings': False,
'user_agent': user_agent,
'socket_timeout': 30,
'extractor_args': {
'youtube': {
'player_client': ['android', 'web'],
'player_skip': ['webpage'],
},
},
'http_headers': {
'User-Agent': user_agent,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-us,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
},
}
# Получаем информацию о видео в executor (неблокирующе)
def extract_info_sync():
with yt_dlp.YoutubeDL(ydl_opts_info) as ydl:
return ydl.extract_info(url, download=False)
loop = asyncio.get_event_loop()
info = await loop.run_in_executor(DOWNLOAD_EXECUTOR, extract_info_sync)
video_title = info.get('title', 'video')
logger.info(f"YouTube: получена информация о видео: {video_title}")
# Скачиваем видео
ydl_opts_download = {
'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
'outtmpl': _safe_filename(video_title, chat_id),
'quiet': False,
'no_warnings': False,
'user_agent': user_agent,
'socket_timeout': 30,
'extractor_args': {
'youtube': {
'player_client': ['android', 'web'],
'player_skip': ['webpage'],
},
},
'http_headers': {
'User-Agent': user_agent,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-us,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
},
}
logger.info(f"YouTube: начинаем скачивание (попытка {attempt + 1}/{max_retries})")
# Скачиваем в executor (неблокирующе)
def download_sync():
with yt_dlp.YoutubeDL(ydl_opts_download) as ydl:
ydl.download([url])
await loop.run_in_executor(DOWNLOAD_EXECUTOR, download_sync)
# Находим скачанный файл (тоже в executor для консистентности)
def find_downloaded_file():
downloaded_files = list(DOWNLOADS_DIR.glob(f'{chat_id}_*'))
if downloaded_files:
downloaded_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
return str(downloaded_files[0])
return None
video_path = await loop.run_in_executor(DOWNLOAD_EXECUTOR, find_downloaded_file)
if video_path:
return video_path
else:
raise Exception("Файл не был найден после скачивания")
except Exception as e:
last_error = e
logger.warning(f"YouTube: попытка {attempt + 1}/{max_retries} не удалась: {e}")
if attempt < max_retries - 1:
await asyncio.sleep((attempt + 1) * 2)
raise last_error or Exception("Неизвестная ошибка при скачивании с YouTube")
async def download_instagram_video(url: str, chat_id: int, max_retries: int = 3) -> str:
"""Скачивает видео с Instagram - используем cookies с правильными заголовками"""
cookies_file = os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt')
cookies_file_path = Path(cookies_file)
# Проверяем срок действия cookies перед использованием
if cookies_file_path.exists():
is_valid, days_left = check_instagram_cookies_expiry()
if not is_valid:
logger.error("Instagram cookies истекли! Необходимо обновить cookies.")
raise Exception("Instagram cookies истекли. Пожалуйста, обновите cookies в файле instagram_cookies.txt")
elif days_left < 7:
logger.warning(f"Instagram cookies истекают через {days_left} дней. Рекомендуется обновить.")
# Парсим cookies для получения csrf token (формат Netscape)
csrf_token = None
sessionid = None
if cookies_file_path.exists():
try:
with open(cookies_file_path, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
parts = line.split('\t')
if len(parts) >= 7:
domain = parts[0]
# Ищем только cookies от instagram.com
if 'instagram' in domain.lower():
cookie_name = parts[5] # Имя cookie
cookie_value = parts[6] # Значение cookie
if cookie_name == 'csrftoken':
csrf_token = cookie_value
elif cookie_name == 'sessionid':
sessionid = cookie_value
# Если нашли оба - можно выходить
if csrf_token and sessionid:
break
except Exception as e:
logger.warning(f"Не удалось прочитать cookies: {e}")
last_error = None
for attempt in range(max_retries):
try:
# Базовые настройки
ydl_opts = {
'format': 'best',
'outtmpl': str(DOWNLOADS_DIR / f'{chat_id}_%(title)s.%(ext)s'),
'quiet': False,
'no_warnings': False,
'socket_timeout': 30,
}
# Если есть файл с cookies, используем его
if cookies_file_path.exists():
# Используем абсолютный путь к cookies
ydl_opts['cookiefile'] = str(cookies_file_path.absolute())
logger.info(f"Instagram: используем cookies из {cookies_file_path}")
# Добавляем заголовки с csrf token если есть
headers = {
'Referer': 'https://www.instagram.com/',
'X-Requested-With': 'XMLHttpRequest',
}
if csrf_token:
headers['X-CSRFToken'] = csrf_token
logger.info(f"Instagram: добавлен csrf token в заголовки")
if sessionid:
logger.info(f"Instagram: sessionid найден (длина: {len(sessionid)})")
ydl_opts['http_headers'] = headers
logger.info(f"Instagram: начинаем скачивание (попытка {attempt + 1}/{max_retries})")
# Скачиваем в executor (неблокирующе)
def download_sync():
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
loop = asyncio.get_event_loop()
await loop.run_in_executor(DOWNLOAD_EXECUTOR, download_sync)
# Находим скачанный файл (тоже в executor для консистентности)
def find_downloaded_file():
downloaded_files = list(DOWNLOADS_DIR.glob(f'{chat_id}_*'))
if downloaded_files:
downloaded_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
return str(downloaded_files[0])
return None
video_path = await loop.run_in_executor(DOWNLOAD_EXECUTOR, find_downloaded_file)
if video_path:
return video_path
else:
raise Exception("Файл не был найден после скачивания")
except Exception as e:
last_error = e
logger.warning(f"Instagram: попытка {attempt + 1}/{max_retries} не удалась: {e}")
if attempt < max_retries - 1:
await asyncio.sleep((attempt + 1) * 2)
raise last_error or Exception("Неизвестная ошибка при скачивании с Instagram. Возможно, нужно обновить cookies.")
async def update_instagram_cookies_from_browser(browser: str = 'chrome') -> bool:
"""
Автоматически обновляет Instagram cookies из браузера
Returns: True если успешно, False если ошибка
"""
cookies_file = os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt')
cookies_file_path = Path(cookies_file)
try:
logger.info(f"Попытка обновления Instagram cookies из браузера {browser}...")
# Пробуем обновить cookies из браузера
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None,
lambda: subprocess.run(
[
'yt-dlp',
'--cookies-from-browser', browser,
'--cookies', str(cookies_file_path.absolute()),
'--no-download',
'https://www.instagram.com/'
],
capture_output=True,
timeout=30,
text=True
)
)
if result.returncode == 0:
logger.info(f"✅ Instagram cookies успешно обновлены из браузера {browser}")
return True
else:
logger.warning(f"Не удалось обновить cookies из {browser}: {result.stderr[:200]}")
return False
except subprocess.TimeoutExpired:
logger.warning(f"Таймаут при обновлении cookies из {browser}")
return False
except FileNotFoundError:
logger.warning(f"yt-dlp не найден для обновления cookies")
return False
except Exception as e:
logger.error(f"Ошибка при обновлении cookies из браузера: {e}")
return False
def check_instagram_cookies_expiry() -> tuple[bool, int]:
"""
Проверяет срок действия Instagram cookies
Returns: (is_valid, days_until_expiry)
"""
cookies_file = os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt')
cookies_file_path = Path(cookies_file)
if not cookies_file_path.exists():
return False, 0
try:
current_time = time.time()
valid_expiries = []
# Важные cookies для Instagram (проверяем их в первую очередь)
important_cookies = ['sessionid', 'csrftoken', 'ds_user_id']
with open(cookies_file_path, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
parts = line.split('\t')
if len(parts) >= 7:
domain = parts[0]
if 'instagram' in domain.lower():
try:
expiry = int(parts[4]) # Unix timestamp
cookie_name = parts[5] if len(parts) > 5 else ''
# Игнорируем невалидные expiry (0, отрицательные, или слишком старые)
# Session cookies (expiry = 0) также игнорируем для проверки срока
if expiry > 0 and expiry > 946684800: # Фильтр: после 2000-01-01 (избегаем epoch 0)
# Для важных cookies проверяем строже
if cookie_name in important_cookies:
if expiry > current_time:
valid_expiries.append(expiry)
else:
valid_expiries.append(expiry)
except (ValueError, IndexError):
continue
if not valid_expiries:
logger.warning("Не найдено валидных Instagram cookies с нормальным сроком действия")
# Если нет валидных expiry, но есть cookies - считаем их действительными
# (возможно, это session cookies)
return True, 30 # Возвращаем разумное значение по умолчанию
# Берем минимальный валидный expiry
min_expiry = min(valid_expiries)
days_until_expiry = (min_expiry - current_time) / 86400
is_valid = min_expiry > current_time
return is_valid, int(days_until_expiry)
except Exception as e:
logger.error(f"Ошибка при проверке срока действия cookies: {e}")
# В случае ошибки считаем cookies действительными (не блокируем работу)
return True, 30
async def keep_instagram_session_alive():
"""Поддерживает сессию Instagram активной через периодические запросы и автоматически обновляет cookies"""
cookies_file = os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt')
cookies_file_path = Path(cookies_file)
AUTO_UPDATE_DAYS_BEFORE_EXPIRY = int(os.getenv('INSTAGRAM_AUTO_UPDATE_DAYS', '3')) # Обновлять за 3 дня до истечения
if not cookies_file_path.exists():
logger.info("Instagram cookies не найдены, пропускаем поддержание сессии")
return
# Список браузеров для попытки обновления (по приоритету)
browsers_to_try = ['chrome', 'firefox', 'edge', 'opera']
# Проверяем cookies при старте
is_valid, days_left = check_instagram_cookies_expiry()
if not is_valid:
logger.warning("Instagram cookies истекли! Пытаемся автоматически обновить...")
# Пытаемся обновить из браузера
updated = False
for browser in browsers_to_try:
if await update_instagram_cookies_from_browser(browser):
updated = True
break
if not updated:
logger.error("Не удалось автоматически обновить cookies! Необходимо обновить вручную.")
return
else:
# Перепроверяем после обновления
is_valid, days_left = check_instagram_cookies_expiry()
if days_left < AUTO_UPDATE_DAYS_BEFORE_EXPIRY:
logger.warning(f"Instagram cookies истекают через {days_left} дней! Пытаемся автоматически обновить...")
# Пытаемся обновить заранее
for browser in browsers_to_try:
if await update_instagram_cookies_from_browser(browser):
# Перепроверяем
_, days_left = check_instagram_cookies_expiry()
logger.info(f"Cookies обновлены! Новый срок: {days_left} дней")
break
else:
logger.info(f"Instagram cookies действительны еще {days_left} дней")
# Интервал проверки: 24 часа (86400 секунд)
check_interval = 86400
while True:
try:
await asyncio.sleep(check_interval)
# Проверяем срок действия перед каждым запросом
is_valid, days_left = check_instagram_cookies_expiry()
# Автоматическое обновление за N дней до истечения
if days_left < AUTO_UPDATE_DAYS_BEFORE_EXPIRY and days_left > 0:
logger.warning(f"Instagram cookies истекают через {days_left} дней. Автоматическое обновление...")
updated = False
for browser in browsers_to_try:
if await update_instagram_cookies_from_browser(browser):
updated = True
_, days_left = check_instagram_cookies_expiry()
logger.info(f"✅ Cookies обновлены автоматически! Новый срок: {days_left} дней")
break
if not updated:
logger.warning("Не удалось автоматически обновить cookies. Попробуйте обновить вручную.")
if not is_valid:
logger.error("Instagram cookies истекли! Пытаемся автоматически обновить...")
updated = False
for browser in browsers_to_try:
if await update_instagram_cookies_from_browser(browser):
updated = True
is_valid, days_left = check_instagram_cookies_expiry()
break
if not updated:
logger.error("Не удалось автоматически обновить cookies! Остановка поддержания сессии.")
break
# Делаем легкий запрос к Instagram для поддержания активности
logger.info("Поддерживаем активность сессии Instagram...")
try:
ydl_opts = {
'cookiefile': str(cookies_file_path.absolute()),
'quiet': True,
'no_warnings': True,
'socket_timeout': 10,
}
def extract_info_sync():
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
return ydl.extract_info('https://www.instagram.com/', download=False)
loop = asyncio.get_event_loop()
await loop.run_in_executor(DOWNLOAD_EXECUTOR, extract_info_sync)
logger.info(f"Сессия Instagram успешно обновлена. Cookies действительны еще {days_left} дней")
except Exception as e:
logger.warning(f"Не удалось обновить сессию Instagram: {e}")
# Продолжаем работу, попробуем в следующий раз
except asyncio.CancelledError:
logger.info("Поддержание сессии Instagram остановлено")
break
except Exception as e:
logger.error(f"Ошибка в задаче поддержания сессии Instagram: {e}")
# Ждем перед следующей попыткой
await asyncio.sleep(3600) # 1 час
async def download_vk_video(url: str, chat_id: int, max_retries: int = 3) -> str:
"""Скачивает видео с VK через внешний сервис"""
logger.info(f"VK: отправка запроса на внешний сервис {VK_DOWNLOADER_URL}")
last_error = None
for attempt in range(max_retries):
try:
async with httpx.AsyncClient(timeout=600.0) as client: # Увеличенный таймаут для VK
# Отправляем запрос на VK сервис
response = await client.post(
f"{VK_DOWNLOADER_URL}/download/stream",
json={"url": url},
headers={"Content-Type": "application/json"}
)
if response.status_code != 200:
error_text = response.text
try:
error_json = response.json()
error_text = error_json.get('error', error_text)
except:
pass
raise Exception(f"VK сервис вернул ошибку {response.status_code}: {error_text}")
# Сохраняем видео во временный файл
video_data = response.content
video_ext = 'mp4' # По умолчанию mp4
# Пробуем определить расширение из заголовков
content_type = response.headers.get('Content-Type', '')
if 'video/' in content_type:
video_ext = content_type.split('/')[-1].split(';')[0]
# Получаем имя файла из заголовка или создаем случайное
filename = response.headers.get('Content-Disposition', '')
if filename and 'filename=' in filename:
video_filename = filename.split('filename=')[1].strip('"\'')
else:
video_filename = f'{chat_id}_vk_video.{video_ext}'
# Сохраняем файл
video_path = DOWNLOADS_DIR / video_filename
with open(video_path, 'wb') as f:
f.write(video_data)
logger.info(f"VK: видео скачано через внешний сервис: {video_path}")
return str(video_path)
except httpx.TimeoutException:
last_error = Exception(f"Таймаут при запросе к VK сервису (попытка {attempt + 1}/{max_retries})")
logger.warning(f"VK: таймаут при запросе к сервису: {last_error}")
except Exception as e:
last_error = e
logger.warning(f"VK: попытка {attempt + 1}/{max_retries} не удалась: {e}")
if attempt < max_retries - 1:
await asyncio.sleep((attempt + 1) * 2)
raise last_error or Exception("Неизвестная ошибка при скачивании с VK через внешний сервис")
async def download_video(url: str, chat_id: int, max_retries: int = 3) -> str:
"""Главная функция скачивания - вызывает нужную функцию в зависимости от источника"""
source = detect_video_source(url)
logger.info(f"Определен источник: {source} для URL: {url}")
if source == 'youtube':
return await download_youtube_video(url, chat_id, max_retries)
elif source == 'instagram':
return await download_instagram_video(url, chat_id, max_retries)
elif source == 'vk':
return await download_vk_video(url, chat_id, max_retries)
else:
raise Exception("Пардон, не умеем работать с этим источником")
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обрабатывает сообщения от пользователей"""
if not update.message or not update.message.text:
return
text = update.message.text.strip()
chat_id = update.message.chat_id
chat_type = update.message.chat.type # 'private', 'group', 'supergroup'
username = update.message.from_user.username if update.message.from_user else None
first_name = update.message.from_user.first_name if update.message.from_user else None
# Добавляем пользователя в статистику при первом взаимодействии
add_user(chat_id, username, first_name)
# Извлекаем все URL из текста
urls = extract_urls_from_text(text)
# Если это личный чат и нет ссылок, отправляем инструкцию
if not urls and chat_type == 'private':
await update.message.reply_text(
"Пожалуйста, отправьте ссылку на видео.\n"
"Поддерживаемые источники:\n"
"• YouTube (youtube.com, youtu.be)\n"
"• Instagram (instagram.com)\n"
"• VK (vk.com)\n\n"
"Для других источников: Пардон, не умеем 😅"
)
return
# Если нет ссылок в группе, просто игнорируем сообщение
if not urls:
return
# Обрабатываем первую найденную ссылку
url = urls[0]
# Проверяем источник до начала обработки
source = detect_video_source(url)
if source == 'unknown':
# В группах не отвечаем на неподдерживаемые источники, чтобы не спамить
if chat_type == 'private':
await update.message.reply_text("Пардон, не умеем работать с этим источником 😅")
return
# Отправляем сообщение о начале обработки
status_message = await update.message.reply_text("🔍 Обрабатываю ссылку...")
try:
# Скачиваем видео
await status_message.edit_text("⬇️ Скачиваю видео...")
video_path = await download_video(url, chat_id)
# Отправляем файл пользователю
await status_message.edit_text("📤 Отправляю видео...")
video_file = open(video_path, 'rb')
caption = f"Видео скачано с @{TELEGRAM_BOT_USERNAME}"
await update.message.reply_video(
video=video_file,
caption=caption,
supports_streaming=True
)
video_file.close()
# Увеличиваем счетчик скачанных видео
increment_downloads()
# Удаляем временный файл
try:
os.remove(video_path)
logger.info(f"Удален временный файл: {video_path}")
except Exception as e:
logger.warning(f"Не удалось удалить файл {video_path}: {e}")
# Удаляем статусное сообщение и исходное сообщение со ссылкой
try:
await status_message.delete()
await update.message.delete()
logger.info(f"Удалено сообщение пользователя с ссылкой (chat_id: {chat_id}, тип чата: {chat_type})")
except Exception as e:
logger.warning(f"Не удалось удалить сообщение: {e}")
# Если не удалось удалить (нет прав), просто логируем
except Exception as e:
logger.error(f"Ошибка: {e}")
error_msg = f"❌ Произошла ошибка при обработке видео:\n{str(e)}"
try:
await status_message.edit_text(error_msg)
except:
# Если status_message не существует, создаем новое сообщение
await update.message.reply_text(error_msg)
# При ошибке тоже пытаемся удалить временные файлы
try:
# Удаляем все .part файлы для этого chat_id
for part_file in DOWNLOADS_DIR.glob(f'{chat_id}_*.part'):
part_file.unlink()
logger.info(f"Удален .part файл после ошибки: {part_file.name}")
except Exception as cleanup_error:
logger.warning(f"Не удалось удалить .part файлы после ошибки: {cleanup_error}")
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обрабатывает команду /start"""
# Добавляем пользователя в статистику
chat_id = update.message.chat_id
username = update.message.from_user.username if update.message.from_user else None
first_name = update.message.from_user.first_name if update.message.from_user else None
add_user(chat_id, username, first_name)
await update.message.reply_text(
"👋 Привет! Я бот для скачивания видео.\n\n"
"Просто отправь мне ссылку на видео, и я скачаю его для тебя.\n\n"
"Поддерживаемые источники:\n"
"• YouTube (youtube.com, youtu.be)\n"
"• Instagram (instagram.com)\n"
"• VK (vk.com)\n\n"
"👥 Работа в группах:\n"
"Добавь меня в группу и дай права администратора (нужно право на удаление сообщений). "
"После этого я буду автоматически находить ссылки на видео в сообщениях участников, "
"скачивать их и отправлять прямо в группу, заменяя исходное сообщение со ссылкой.\n\n"
"Команды:\n"
"/start - Начать работу\n"
"/stat - Статистика скачанных видео\n\n"
"Отправь ссылку на видео:"
)
async def stat_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обрабатывает команду /stat"""
total_downloads = get_total_downloads()
total_users = get_total_users()
await update.message.reply_text(
f"📊 Статистика бота:\n\n"
f"👥 Всего пользователей: {total_users}\n"
f"📹 Всего скачано видео: {total_downloads}"
)
def main():
"""Главная функция для запуска бота"""
if not TELEGRAM_BOT_TOKEN:
logger.error("TELEGRAM_BOT_TOKEN не установлен!")
return
# Инициализируем базу данных
init_database()
# Очищаем старые файлы при старте
logger.info("Очистка старых файлов при старте...")
cleanup_old_files(max_age_hours=1) # Удаляем файлы старше 1 часа
# Создаем приложение
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
# Регистрируем обработчики
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
application.add_handler(CommandHandler("start", start_command))
application.add_handler(CommandHandler("stat", stat_command))
# Запускаем фоновую задачу для поддержания сессии Instagram
async def post_init(application: Application):
"""Выполняется после инициализации приложения"""
# Запускаем задачу поддержания сессии Instagram в фоне
asyncio.create_task(keep_instagram_session_alive())
logger.info("Фоновая задача поддержания сессии Instagram запущена")
# Запускаем периодическую очистку файлов (каждые 6 часов)
async def periodic_cleanup():
while True:
await asyncio.sleep(6 * 3600) # 6 часов
cleanup_old_files(max_age_hours=1)
logger.info("Периодическая очистка старых файлов выполнена")
asyncio.create_task(periodic_cleanup())
logger.info("Фоновая задача периодической очистки файлов запущена")
application.post_init = post_init
# Запускаем бота
logger.info("Бот запущен")
application.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == '__main__':
main()