мультипоток. очистка файлов. дополнил описание /start про группы
This commit is contained in:
parent
5acd8fd9db
commit
8024eea868
2 changed files with 121 additions and 31 deletions
120
bot.py
120
bot.py
|
|
@ -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,8 +239,13 @@ async def download_youtube_video(url: str, chat_id: int, max_retries: int = 3) -
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Получаем информацию о видео в executor (неблокирующе)
|
||||||
|
def extract_info_sync():
|
||||||
with yt_dlp.YoutubeDL(ydl_opts_info) as ydl:
|
with yt_dlp.YoutubeDL(ydl_opts_info) as ydl:
|
||||||
info = ydl.extract_info(url, download=False)
|
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')
|
video_title = info.get('title', 'video')
|
||||||
logger.info(f"YouTube: получена информация о видео: {video_title}")
|
logger.info(f"YouTube: получена информация о видео: {video_title}")
|
||||||
|
|
||||||
|
|
@ -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 (неблокирующе)
|
||||||
|
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}_*'))
|
downloaded_files = list(DOWNLOADS_DIR.glob(f'{chat_id}_*'))
|
||||||
if downloaded_files:
|
if downloaded_files:
|
||||||
downloaded_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
downloaded_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
||||||
return str(downloaded_files[0])
|
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})")
|
||||||
|
|
||||||
|
# Скачиваем в executor (неблокирующе)
|
||||||
|
def download_sync():
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
loop = asyncio.get_event_loop()
|
ydl.download([url])
|
||||||
await loop.run_in_executor(None, lambda: 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}_*'))
|
downloaded_files = list(DOWNLOADS_DIR.glob(f'{chat_id}_*'))
|
||||||
if downloaded_files:
|
if downloaded_files:
|
||||||
downloaded_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
downloaded_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
||||||
return str(downloaded_files[0])
|
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)}"
|
||||||
|
try:
|
||||||
await status_message.edit_text(error_msg)
|
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()
|
||||||
|
|
||||||
|
|
@ -792,6 +872,16 @@ def main():
|
||||||
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
|
||||||
|
|
||||||
# Запускаем бота
|
# Запускаем бота
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue