ebany_video_bot/bot.py
Vovan999222 1247008941
Enhance media download support for SoundCloud
Added support for SoundCloud links and cover downloads. Updated bot messages and download options.
2026-04-08 15:08:37 +03:00

212 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import os
import re
import logging
from logging.handlers import TimedRotatingFileHandler
import yt_dlp
from config import TOKEN
from aiogram import Bot, Dispatcher, F, types
from aiogram.filters import CommandStart
from aiogram.types import FSInputFile, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.types import FSInputFile, URLInputFile, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.context import FSMContext
from aiogram.exceptions import TelegramEntityTooLarge
os.makedirs('logs', exist_ok=True)
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
log_filename = 'logs/bot-latest.log'
file_handler = TimedRotatingFileHandler(
filename=log_filename,
when='midnight',
interval=1,
backupCount=30,
encoding='utf-8'
)
file_handler.suffix = "%Y-%m-%d-%H-%M-%S"
def log_namer(default_name):
base_dir, filename = os.path.split(default_name)
clean_date = filename.replace("bot-latest.log.", "")
new_filename = f"bot-{clean_date}.log"
return os.path.join(base_dir, new_filename)
file_handler.namer = log_namer
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(formatter)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
if logger.hasHandlers():
logger.handlers.clear()
logger.addHandler(file_handler)
logger.addHandler(console_handler)
bot = Bot(token=TOKEN)
dp = Dispatcher()
def get_user_display_name(user: types.User):
if user.username:
return f"@{user.username}"
return user.first_name
def get_cover_info(url: str):
"""Извлекает прямую ссылку на обложку и название медиа"""
ydl_opts = {'quiet': True, 'no_warnings': True}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=False)
return info.get('thumbnail'), info.get('title', 'Cover')
def download_media(url: str, mode: str) -> str:
"""Синхронная функция скачивания через yt-dlp (видео или аудио)"""
ydl_opts = {
'outtmpl': '%(id)s.%(ext)s',
'quiet': True,
'no_warnings': True,
}
if mode == 'video':
ydl_opts['format'] = 'bestvideo[ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/best[ext=mp4][vcodec^=avc]/best[ext=mp4]/best'
ydl_opts['merge_output_format'] = 'mp4'
else:
ydl_opts['format'] = 'bestaudio/best'
ydl_opts['writethumbnail'] = True
ydl_opts['postprocessors'] = [
{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '320',
},
{
'key': 'EmbedThumbnail',
}
]
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=True)
filename = ydl.prepare_filename(info)
if mode == 'audio':
base_name, _ = os.path.splitext(filename)
filename = f"{base_name}.mp3"
return filename
@dp.message(CommandStart())
async def cmd_start(message: types.Message):
user = message.from_user
name = get_user_display_name(user)
logger.info(f"[{user.id}] {name} начал использовать бота.")
await message.answer(
"Привет! Я бот для скачивания медиа.\n"
"Отправь мне ссылку на YouTube, TikTok, Instagram или SoundCloud, и я предложу форматы для загрузки!"
)
@dp.message(F.text.regexp(r'(?i)https?://(?:[a-zA-Z0-9-]+\.)*(tiktok\.com|youtube\.com|youtu\.be|instagram\.com|soundcloud\.com)/.*'))
async def handle_media_link(message: types.Message, state: FSMContext):
url = message.text
user = message.from_user
name = get_user_display_name(user)
logger.info(f"[{user.id}] {name} прислал ссылку: {url}")
if 'soundcloud.com' in url.lower():
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🖼 Скачать обложку трека", callback_data='cover')],
[InlineKeyboardButton(text="🎵 Скачать трек", callback_data='audio')]
])
else:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📹 Скачать видео", callback_data='video')],
[InlineKeyboardButton(text="🎵 Скачать аудио", callback_data='audio')]
])
await state.update_data(media_url=url)
await message.answer("Что именно нужно скачать?", reply_markup=keyboard)
@dp.callback_query(F.data.in_({"video", "audio", "cover"}))
async def button_callback(callback: types.CallbackQuery, state: FSMContext):
await callback.answer()
user = callback.from_user
name = get_user_display_name(user)
mode = callback.data
logger.info(f"[{user.id}] {name} нажал кнопку: {mode}")
data = await state.get_data()
media_url = data.get('media_url')
if not media_url:
await callback.message.edit_text("❌ Ссылка устарела. Отправьте её заново.")
return
await state.clear()
status_text = {
'video': "⏳ Скачиваю видео...",
'audio': "⏳ Скачиваю аудио...",
'cover': "⏳ Получаю обложку..."
}.get(mode, "⏳ Обработка...")
status_msg = await callback.message.edit_text(status_text)
try:
if mode == 'cover':
cover_url, title = await asyncio.to_thread(get_cover_info, media_url)
if cover_url:
safe_title = re.sub(r'[\\/*?:"<>|]', "", title)
cover_doc = URLInputFile(url=cover_url, filename=f"{safe_title}.jpg")
await callback.message.answer_document(
document=cover_doc,
disable_content_type_detection=True
)
else:
await callback.message.answer("Не удалось найти обложку для этой ссылки.")
await status_msg.delete()
return
file_path = await asyncio.to_thread(download_media, media_url, mode)
try:
if mode == 'video':
document = FSInputFile(file_path)
await callback.message.answer_document(
document=document,
disable_content_type_detection=True
)
elif mode == 'audio':
audio = FSInputFile(file_path)
await callback.message.answer_audio(
audio=audio
)
logger.info(f"[{user.id}] {name} -> Успешно отправлен файл {mode}.")
await status_msg.delete()
except TelegramEntityTooLarge:
await status_msg.edit_text("❌ Файл слишком большой! Telegram позволяет отправлять ботам файлы размером не более 50 МБ.")
logger.warning(f"[{user.id}] {name} -> Файл превысил лимит в 50 МБ.")
finally:
if os.path.exists(file_path):
os.remove(file_path)
except Exception as e:
logger.error(f"Error process_media [{mode}]: {e}")
await status_msg.edit_text("❌ Произошла ошибка. Возможно, видео/трек скрыто приватностью, удалено или временно недоступно.")
@dp.message()
async def handle_other_messages(message: types.Message):
await message.answer("Пожалуйста, отправьте корректную ссылку на YouTube, TikTok, Instagram или SoundCloud.")
async def main():
print(f"Активный лог: {log_filename}")
print("Бот запущен")
await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot)
if __name__ == '__main__':
try:
asyncio.run(main())
except (KeyboardInterrupt, SystemExit):
print("Бот остановлен пользователем.")