Enhance media download support for SoundCloud

Added support for SoundCloud links and cover downloads. Updated bot messages and download options.
This commit is contained in:
Vovan999222 2026-04-08 15:08:37 +03:00 committed by GitHub
parent 036567a061
commit 1247008941
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

80
bot.py
View file

@ -8,6 +8,7 @@ from config import TOKEN
from aiogram import Bot, Dispatcher, F, types from aiogram import Bot, Dispatcher, F, types
from aiogram.filters import CommandStart from aiogram.filters import CommandStart
from aiogram.types import FSInputFile, InlineKeyboardMarkup, InlineKeyboardButton from aiogram.types import FSInputFile, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.types import FSInputFile, URLInputFile, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.exceptions import TelegramEntityTooLarge from aiogram.exceptions import TelegramEntityTooLarge
@ -54,9 +55,15 @@ def get_user_display_name(user: types.User):
return f"@{user.username}" return f"@{user.username}"
return user.first_name 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: def download_media(url: str, mode: str) -> str:
"""Синхронная функция скачивания через yt-dlp (видео или аудио)""" """Синхронная функция скачивания через yt-dlp (видео или аудио)"""
ydl_opts = { ydl_opts = {
'outtmpl': '%(id)s.%(ext)s', 'outtmpl': '%(id)s.%(ext)s',
'quiet': True, 'quiet': True,
@ -65,13 +72,20 @@ def download_media(url: str, mode: str) -> str:
if mode == 'video': 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['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: else:
ydl_opts['format'] = 'bestaudio/best' ydl_opts['format'] = 'bestaudio/best'
ydl_opts['postprocessors'] = [{ ydl_opts['writethumbnail'] = True
'key': 'FFmpegExtractAudio', ydl_opts['postprocessors'] = [
'preferredcodec': 'mp3', {
'preferredquality': '320', 'key': 'FFmpegExtractAudio',
}] 'preferredcodec': 'mp3',
'preferredquality': '320',
},
{
'key': 'EmbedThumbnail',
}
]
with yt_dlp.YoutubeDL(ydl_opts) as ydl: with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=True) info = ydl.extract_info(url, download=True)
@ -88,24 +102,32 @@ async def cmd_start(message: types.Message):
logger.info(f"[{user.id}] {name} начал использовать бота.") logger.info(f"[{user.id}] {name} начал использовать бота.")
await message.answer( await message.answer(
"Привет! Я бот для скачивания из TikTok, YouTube или Instagram.\n" "Привет! Я бот для скачивания медиа.\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)/.*')) @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): async def handle_media_link(message: types.Message, state: FSMContext):
url = message.text url = message.text
user = message.from_user user = message.from_user
name = get_user_display_name(user) name = get_user_display_name(user)
logger.info(f"[{user.id}] {name} прислал ссылку: {url}") logger.info(f"[{user.id}] {name} прислал ссылку: {url}")
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📹 Скачать видео", callback_data='video')], if 'soundcloud.com' in url.lower():
[InlineKeyboardButton(text="🎵 Скачать аудио", callback_data='audio')] 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 state.update_data(media_url=url)
await message.answer("Что именно нужно скачать?", reply_markup=keyboard) await message.answer("Что именно нужно скачать?", reply_markup=keyboard)
@dp.callback_query(F.data.in_({"video", "audio"})) @dp.callback_query(F.data.in_({"video", "audio", "cover"}))
async def button_callback(callback: types.CallbackQuery, state: FSMContext): async def button_callback(callback: types.CallbackQuery, state: FSMContext):
await callback.answer() await callback.answer()
@ -122,10 +144,29 @@ async def button_callback(callback: types.CallbackQuery, state: FSMContext):
return return
await state.clear() await state.clear()
status_text = "⏳ Скачиваю видео..." if mode == 'video' else "⏳ Скачиваю аудио..." status_text = {
await callback.message.edit_text(status_text) 'video': "⏳ Скачиваю видео...",
'audio': "⏳ Скачиваю аудио...",
'cover': "⏳ Получаю обложку..."
}.get(mode, "⏳ Обработка...")
status_msg = await callback.message.edit_text(status_text)
try: 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) file_path = await asyncio.to_thread(download_media, media_url, mode)
try: try:
@ -141,9 +182,10 @@ async def button_callback(callback: types.CallbackQuery, state: FSMContext):
audio=audio audio=audio
) )
logger.info(f"[{user.id}] {name} -> Успешно отправлен файл {mode}.") logger.info(f"[{user.id}] {name} -> Успешно отправлен файл {mode}.")
await status_msg.delete()
except TelegramEntityTooLarge: except TelegramEntityTooLarge:
await callback.message.answer("❌ Файл слишком большой! Telegram позволяет отправлять ботам файлы размером не более 50 МБ.") await status_msg.edit_text("❌ Файл слишком большой! Telegram позволяет отправлять ботам файлы размером не более 50 МБ.")
logger.warning(f"[{user.id}] {name} -> Файл превысил лимит в 50 МБ.") logger.warning(f"[{user.id}] {name} -> Файл превысил лимит в 50 МБ.")
finally: finally:
if os.path.exists(file_path): if os.path.exists(file_path):
@ -151,11 +193,11 @@ async def button_callback(callback: types.CallbackQuery, state: FSMContext):
except Exception as e: except Exception as e:
logger.error(f"Error process_media [{mode}]: {e}") logger.error(f"Error process_media [{mode}]: {e}")
await callback.message.answer("❌ Произошла ошибка. Возможно, видео скрыто приватностью, удалено или временно недоступно.") await status_msg.edit_text("❌ Произошла ошибка. Возможно, видео/трек скрыто приватностью, удалено или временно недоступно.")
@dp.message() @dp.message()
async def handle_other_messages(message: types.Message): async def handle_other_messages(message: types.Message):
await message.answer("Пожалуйста, отправьте корректную ссылку на TikTok, YouTube или Instagram.") await message.answer("Пожалуйста, отправьте корректную ссылку на YouTube, TikTok, Instagram или SoundCloud.")
async def main(): async def main():
print(f"Активный лог: {log_filename}") print(f"Активный лог: {log_filename}")