мультипоток. очистка файлов. дополнил описание /start про группы

This commit is contained in:
vrubelroman 2025-12-10 21:05:27 +03:00
parent 5acd8fd9db
commit 8024eea868
2 changed files with 121 additions and 31 deletions

146
bot.py
View file

@ -9,6 +9,7 @@ import subprocess
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
from datetime import datetime from datetime import datetime
from concurrent.futures import ThreadPoolExecutor
import yt_dlp import yt_dlp
import httpx import httpx
@ -40,6 +41,10 @@ DATA_DIR = BASE_DIR / 'data'
DATA_DIR.mkdir(parents=True, exist_ok=True) DATA_DIR.mkdir(parents=True, exist_ok=True)
DB_FILE = DATA_DIR / 'bot.db' DB_FILE = DATA_DIR / 'bot.db'
# ThreadPoolExecutor для выполнения блокирующих операций (скачивание видео)
# Позволяет обрабатывать несколько запросов параллельно
DOWNLOAD_EXECUTOR = ThreadPoolExecutor(max_workers=5, thread_name_prefix="download")
def init_database(): def init_database():
"""Инициализирует базу данных и создает таблицы если их нет""" """Инициализирует базу данных и создает таблицы если их нет"""
try: try:
@ -169,6 +174,37 @@ def extract_urls_from_text(text: str) -> list[str]:
return urls 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: def _safe_filename(title: str, chat_id: int) -> str:
"""Создает безопасное имя файла""" """Создает безопасное имя файла"""
safe_title = re.sub(r'[<>:"/\\|?*]', '', title)[:100] safe_title = re.sub(r'[<>:"/\\|?*]', '', title)[:100]
@ -203,10 +239,15 @@ async def download_youtube_video(url: str, chat_id: int, max_retries: int = 3) -
}, },
} }
with yt_dlp.YoutubeDL(ydl_opts_info) as ydl: # Получаем информацию о видео в executor (неблокирующе)
info = ydl.extract_info(url, download=False) def extract_info_sync():
video_title = info.get('title', 'video') with yt_dlp.YoutubeDL(ydl_opts_info) as ydl:
logger.info(f"YouTube: получена информация о видео: {video_title}") 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 = { ydl_opts_download = {
@ -232,15 +273,25 @@ async def download_youtube_video(url: str, chat_id: int, max_retries: int = 3) -
} }
logger.info(f"YouTube: начинаем скачивание (попытка {attempt + 1}/{max_retries})") logger.info(f"YouTube: начинаем скачивание (попытка {attempt + 1}/{max_retries})")
with yt_dlp.YoutubeDL(ydl_opts_download) as ydl:
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, lambda: ydl.download([url]))
# Находим скачанный файл # Скачиваем в executor (неблокирующе)
downloaded_files = list(DOWNLOADS_DIR.glob(f'{chat_id}_*')) def download_sync():
if downloaded_files: with yt_dlp.YoutubeDL(ydl_opts_download) as ydl:
downloaded_files.sort(key=lambda x: x.stat().st_mtime, reverse=True) ydl.download([url])
return str(downloaded_files[0])
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: else:
raise Exception("Файл не был найден после скачивания") raise Exception("Файл не был найден после скачивания")
@ -327,15 +378,25 @@ async def download_instagram_video(url: str, chat_id: int, max_retries: int = 3)
logger.info(f"Instagram: начинаем скачивание (попытка {attempt + 1}/{max_retries})") logger.info(f"Instagram: начинаем скачивание (попытка {attempt + 1}/{max_retries})")
with yt_dlp.YoutubeDL(ydl_opts) as ydl: # Скачиваем в executor (неблокирующе)
loop = asyncio.get_event_loop() def download_sync():
await loop.run_in_executor(None, lambda: ydl.download([url])) with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
# Находим скачанный файл loop = asyncio.get_event_loop()
downloaded_files = list(DOWNLOADS_DIR.glob(f'{chat_id}_*')) await loop.run_in_executor(DOWNLOAD_EXECUTOR, download_sync)
if downloaded_files:
downloaded_files.sort(key=lambda x: x.stat().st_mtime, reverse=True) # Находим скачанный файл (тоже в executor для консистентности)
return str(downloaded_files[0]) 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: else:
raise Exception("Файл не был найден после скачивания") raise Exception("Файл не был найден после скачивания")
@ -546,14 +607,12 @@ async def keep_instagram_session_alive():
'socket_timeout': 10, '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() loop = asyncio.get_event_loop()
await loop.run_in_executor( await loop.run_in_executor(DOWNLOAD_EXECUTOR, extract_info_sync)
None,
lambda: yt_dlp.YoutubeDL(ydl_opts).extract_info(
'https://www.instagram.com/',
download=False
)
)
logger.info(f"Сессия Instagram успешно обновлена. Cookies действительны еще {days_left} дней") logger.info(f"Сессия Instagram успешно обновлена. Cookies действительны еще {days_left} дней")
except Exception as e: except Exception as e:
@ -731,7 +790,20 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
except Exception as e: except Exception as e:
logger.error(f"Ошибка: {e}") logger.error(f"Ошибка: {e}")
error_msg = f"❌ Произошла ошибка при обработке видео:\n{str(e)}" error_msg = f"❌ Произошла ошибка при обработке видео:\n{str(e)}"
await status_message.edit_text(error_msg) 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): async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
@ -749,6 +821,10 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"• YouTube (youtube.com, youtu.be)\n" "• YouTube (youtube.com, youtu.be)\n"
"• Instagram (instagram.com)\n" "• Instagram (instagram.com)\n"
"• VK (vk.com)\n\n" "• VK (vk.com)\n\n"
"👥 Работа в группах:\n"
"Добавь меня в группу и дай права администратора (нужно право на удаление сообщений). "
"После этого я буду автоматически находить ссылки на видео в сообщениях участников, "
"скачивать их и отправлять прямо в группу, заменяя исходное сообщение со ссылкой.\n\n"
"Команды:\n" "Команды:\n"
"/start - Начать работу\n" "/start - Начать работу\n"
"/stat - Статистика скачанных видео\n\n" "/stat - Статистика скачанных видео\n\n"
@ -777,6 +853,10 @@ def main():
# Инициализируем базу данных # Инициализируем базу данных
init_database() init_database()
# Очищаем старые файлы при старте
logger.info("Очистка старых файлов при старте...")
cleanup_old_files(max_age_hours=1) # Удаляем файлы старше 1 часа
# Создаем приложение # Создаем приложение
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build() application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
@ -791,6 +871,16 @@ def main():
# Запускаем задачу поддержания сессии Instagram в фоне # Запускаем задачу поддержания сессии Instagram в фоне
asyncio.create_task(keep_instagram_session_alive()) asyncio.create_task(keep_instagram_session_alive())
logger.info("Фоновая задача поддержания сессии Instagram запущена") 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 application.post_init = post_init

View file

@ -39,13 +39,13 @@ rusoska.com FALSE / FALSE 1799795493 userToken a01e24c3-c94f-4a4e-b11b-751b72046
.mozilla.org TRUE / FALSE 1799925684 _ga_B9CY1C9VBC GS2.1.s1765365263$o1$g1$t1765365684$j60$l0$h0 .mozilla.org TRUE / FALSE 1799925684 _ga_B9CY1C9VBC GS2.1.s1765365263$o1$g1$t1765365684$j60$l0$h0
.mozilla.org TRUE / FALSE 1799925263 _ga GA1.2.1451822324.1765365263 .mozilla.org TRUE / FALSE 1799925263 _ga GA1.2.1451822324.1765365263
.mozilla.org TRUE / FALSE 1765451663 _gid GA1.2.878207985.1765365263 .mozilla.org TRUE / FALSE 1765451663 _gid GA1.2.878207985.1765365263
.instagram.com TRUE / TRUE 1799947196 csrftoken CnChQ6nTz8cfm_U7q2ur9w .instagram.com TRUE / TRUE 1799949717 csrftoken CnChQ6nTz8cfm_U7q2ur9w
.instagram.com TRUE / TRUE 1799925292 datr LFY5aVDEvvzQRTypNm_NZ0d3 .instagram.com TRUE / TRUE 1799925292 datr LFY5aVDEvvzQRTypNm_NZ0d3
.instagram.com TRUE / TRUE 1796901312 ig_did B0879634-89D6-4098-9B3E-958B6BC00183 .instagram.com TRUE / TRUE 1796901312 ig_did B0879634-89D6-4098-9B3E-958B6BC00183
.instagram.com TRUE / TRUE 1765970112 dpr 2 .instagram.com TRUE / TRUE 1765970112 dpr 2
.instagram.com TRUE / TRUE 1799925293 mid aTlWLAAEAAEBRoS_PfrA_i5UP0w1 .instagram.com TRUE / TRUE 1799925293 mid aTlWLAAEAAEBRoS_PfrA_i5UP0w1
.instagram.com TRUE / TRUE 1765980504 wd 1920x944 .instagram.com TRUE / TRUE 1765980504 wd 1920x944
.instagram.com TRUE / TRUE 1796911697 sessionid 42059678244%3AD0GdfKmaFZWqXp%3A10%3AAYieDJrvoWIE9WW--tzjgv-3EyrgI9XT6seopSdHFw .instagram.com TRUE / TRUE 1796911697 sessionid 42059678244%3AD0GdfKmaFZWqXp%3A10%3AAYieDJrvoWIE9WW--tzjgv-3EyrgI9XT6seopSdHFw
.instagram.com TRUE / TRUE 1773163196 ds_user_id 42059678244 .instagram.com TRUE / TRUE 1773165717 ds_user_id 42059678244
.instagram.com TRUE / TRUE 0 rur "LDC\05442059678244\0541796923196:01fea3f1fb8a6a9f2cc556e3847d913f9a142263fa428807624c02963fddee2a5d36b728" .instagram.com TRUE / TRUE 0 rur "LDC\05442059678244\0541796925717:01fef99bf0a6a2eec6207d44260971856a393246d5f7590afa05e8b7183b7b873f243693"
addons.mozilla.org FALSE / TRUE 0 taarId 4dffa50e49cca797bb48f2f4f11803c251746ad45af1fef3ba1ad37379a24fea addons.mozilla.org FALSE / TRUE 0 taarId 4dffa50e49cca797bb48f2f4f11803c251746ad45af1fef3ba1ad37379a24fea