482 lines
19 KiB
Python
482 lines
19 KiB
Python
|
|
"""User-bot для обработки запросов пользователей."""
|
|||
|
|
import asyncio
|
|||
|
|
import logging
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import Optional
|
|||
|
|
|
|||
|
|
from telegram import Update
|
|||
|
|
from telegram.ext import (
|
|||
|
|
Application,
|
|||
|
|
CommandHandler,
|
|||
|
|
MessageHandler,
|
|||
|
|
filters,
|
|||
|
|
ContextTypes,
|
|||
|
|
ConversationHandler,
|
|||
|
|
)
|
|||
|
|
from telegram.request import HTTPXRequest
|
|||
|
|
|
|||
|
|
from app.config import Config
|
|||
|
|
from app.queue_manager import QueueManager, Task
|
|||
|
|
from app.youtube_downloader import is_youtube_url, get_video_title, download_and_convert, sanitize_filename
|
|||
|
|
from app.admin_manager import AdminManager
|
|||
|
|
from app.statistics import Statistics
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
# Состояния для ConversationHandler (ожидание имени файла)
|
|||
|
|
WAITING_FOR_FILENAME = 1
|
|||
|
|
|
|||
|
|
# Хранилище ожидающих ответа пользователей: user_id -> (url, output_path, event, result_container)
|
|||
|
|
pending_filename_requests = {}
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def send_status_message(context: ContextTypes.DEFAULT_TYPE, chat_id: int, text: str) -> Optional[int]:
|
|||
|
|
"""Отправить сообщение о статусе и вернуть message_id."""
|
|||
|
|
try:
|
|||
|
|
message = await context.bot.send_message(chat_id=chat_id, text=text)
|
|||
|
|
return message.message_id
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"Error sending status message: {e}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def process_task(task: Task, config: Config, admin_manager: AdminManager, admin_bot_app: Application) -> str:
|
|||
|
|
"""
|
|||
|
|
Обработать задачу: скачать видео и сконвертировать в MP3.
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Путь к созданному MP3 файлу
|
|||
|
|
"""
|
|||
|
|
# Получаем название видео
|
|||
|
|
title = await get_video_title(task.url, config=config)
|
|||
|
|
|
|||
|
|
# Если название не получено, запрашиваем у пользователя
|
|||
|
|
if not title:
|
|||
|
|
# Сохраняем информацию о запросе
|
|||
|
|
output_path = config.workdir / f"task_{task.task_id}"
|
|||
|
|
pending_filename_requests[task.user_id] = (task.url, output_path)
|
|||
|
|
|
|||
|
|
# Отправляем запрос пользователю
|
|||
|
|
status_callback = task.callback
|
|||
|
|
if status_callback:
|
|||
|
|
await status_callback("Не смог определить название. Введи имя файла (без расширения .mp3).")
|
|||
|
|
|
|||
|
|
# Ждём ответа пользователя (таймаут 5 минут)
|
|||
|
|
try:
|
|||
|
|
custom_title = await asyncio.wait_for(
|
|||
|
|
_wait_for_filename(task.user_id),
|
|||
|
|
timeout=300.0 # 5 минут
|
|||
|
|
)
|
|||
|
|
title = custom_title
|
|||
|
|
except asyncio.TimeoutError:
|
|||
|
|
raise Exception("Таймаут ожидания имени файла (5 минут)")
|
|||
|
|
finally:
|
|||
|
|
# Удаляем из ожидающих
|
|||
|
|
pending_filename_requests.pop(task.user_id, None)
|
|||
|
|
|
|||
|
|
# Формируем безопасное имя файла
|
|||
|
|
safe_title = sanitize_filename(title)
|
|||
|
|
output_path = config.workdir / f"task_{task.task_id}_{safe_title}"
|
|||
|
|
|
|||
|
|
# Скачиваем и конвертируем
|
|||
|
|
mp3_path = await download_and_convert(task.url, output_path, custom_title=safe_title, config=config)
|
|||
|
|
|
|||
|
|
return str(mp3_path)
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _wait_for_filename(user_id: int) -> str:
|
|||
|
|
"""
|
|||
|
|
Ожидание ответа пользователя с именем файла.
|
|||
|
|
Используется asyncio.Event для синхронизации.
|
|||
|
|
"""
|
|||
|
|
event = asyncio.Event()
|
|||
|
|
result_container = {'value': None}
|
|||
|
|
|
|||
|
|
# Сохраняем event в глобальном словаре для доступа из handler
|
|||
|
|
if user_id not in pending_filename_requests:
|
|||
|
|
raise Exception("Request not found")
|
|||
|
|
|
|||
|
|
# Получаем существующую запись и добавляем event
|
|||
|
|
url, output_path = pending_filename_requests[user_id]
|
|||
|
|
pending_filename_requests[user_id] = (url, output_path, event, result_container)
|
|||
|
|
|
|||
|
|
# Ждём события
|
|||
|
|
await event.wait()
|
|||
|
|
|
|||
|
|
if result_container['value'] is None:
|
|||
|
|
raise Exception("No filename received")
|
|||
|
|
|
|||
|
|
return result_container['value']
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def handle_filename_response(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||
|
|
"""Обработка ответа пользователя с именем файла."""
|
|||
|
|
user_id = update.effective_user.id
|
|||
|
|
text = update.message.text.strip()
|
|||
|
|
language = get_user_language(update)
|
|||
|
|
|
|||
|
|
if user_id in pending_filename_requests:
|
|||
|
|
try:
|
|||
|
|
url, output_path, event, result_container = pending_filename_requests[user_id]
|
|||
|
|
result_container['value'] = text
|
|||
|
|
event.set()
|
|||
|
|
|
|||
|
|
confirm_msg = f"Использую имя: {text}.mp3" if language == 'ru' else f"Using name: {text}.mp3"
|
|||
|
|
await update.message.reply_text(confirm_msg)
|
|||
|
|
except (ValueError, KeyError):
|
|||
|
|
# Если структура не та, что ожидалась
|
|||
|
|
error_msg = "Ошибка обработки имени файла." if language == 'ru' else "Error processing filename."
|
|||
|
|
await update.message.reply_text(error_msg)
|
|||
|
|
return ConversationHandler.END
|
|||
|
|
else:
|
|||
|
|
# Если нет активного запроса, проверяем, не является ли это ссылкой
|
|||
|
|
return await handle_message(update, context)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_user_language(update: Update) -> str:
|
|||
|
|
"""Определить язык пользователя для локализации."""
|
|||
|
|
user = update.effective_user
|
|||
|
|
lang_code = user.language_code or 'en'
|
|||
|
|
# Если язык русский или начинается с ru, возвращаем 'ru', иначе 'en'
|
|||
|
|
return 'ru' if lang_code.startswith('ru') else 'en'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_start_message(language: str) -> str:
|
|||
|
|
"""Получить приветственное сообщение в зависимости от языка."""
|
|||
|
|
if language == 'ru':
|
|||
|
|
return (
|
|||
|
|
"Привет! 👋\n\n"
|
|||
|
|
"Я бот для скачивания аудио из YouTube видео.\n\n"
|
|||
|
|
"Просто отправь мне ссылку на YouTube видео, и я верну тебе MP3 файл с аудиодорожкой.\n\n"
|
|||
|
|
"Поддерживаются ссылки:\n"
|
|||
|
|
"• https://www.youtube.com/...\n"
|
|||
|
|
"• https://youtu.be/..."
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
return (
|
|||
|
|
"Hello! 👋\n\n"
|
|||
|
|
"I'm a bot for downloading audio from YouTube videos.\n\n"
|
|||
|
|
"Just send me a link to a YouTube video, and I'll return an MP3 file with the audio track.\n\n"
|
|||
|
|
"Supported links:\n"
|
|||
|
|
"• https://www.youtube.com/...\n"
|
|||
|
|
"• https://youtu.be/..."
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||
|
|
"""Обработчик команды /start для user-bot."""
|
|||
|
|
user = update.effective_user
|
|||
|
|
language = get_user_language(update)
|
|||
|
|
message = get_start_message(language)
|
|||
|
|
|
|||
|
|
# Добавляем пользователя в статистику
|
|||
|
|
statistics = context.bot_data.get('statistics')
|
|||
|
|
if statistics:
|
|||
|
|
statistics.add_user(user.id)
|
|||
|
|
|
|||
|
|
await update.message.reply_text(message)
|
|||
|
|
logger.info(f"User {user.id} (@{user.username}) started the bot (language: {language})")
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||
|
|
"""Обработка сообщения от пользователя."""
|
|||
|
|
user = update.effective_user
|
|||
|
|
message = update.message
|
|||
|
|
|
|||
|
|
# Если пользователь ожидает ввода имени файла, не обрабатываем как ссылку
|
|||
|
|
if user.id in pending_filename_requests:
|
|||
|
|
# Это обработается в handle_filename_response
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
text = message.text.strip()
|
|||
|
|
language = get_user_language(update)
|
|||
|
|
|
|||
|
|
# Проверяем, является ли это ссылкой на YouTube
|
|||
|
|
if not is_youtube_url(text):
|
|||
|
|
error_msg = "Пришли ссылку на YouTube-видео." if language == 'ru' else "Please send a YouTube video link."
|
|||
|
|
await message.reply_text(error_msg)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# Получаем queue_manager из context
|
|||
|
|
queue_manager: QueueManager = context.bot_data.get('queue_manager')
|
|||
|
|
config: Config = context.bot_data.get('config')
|
|||
|
|
admin_manager: AdminManager = context.bot_data.get('admin_manager')
|
|||
|
|
admin_bot_app: Application = context.bot_data.get('admin_bot_app')
|
|||
|
|
|
|||
|
|
if not queue_manager:
|
|||
|
|
error_msg = "Ошибка: очередь не инициализирована." if language == 'ru' else "Error: queue not initialized."
|
|||
|
|
await message.reply_text(error_msg)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# Добавляем пользователя в статистику
|
|||
|
|
statistics = context.bot_data.get('statistics')
|
|||
|
|
if statistics:
|
|||
|
|
statistics.add_user(user.id)
|
|||
|
|
|
|||
|
|
status_message_ids: list[int] = []
|
|||
|
|
|
|||
|
|
# Callback для отправки статуса
|
|||
|
|
async def status_callback(status_text: str):
|
|||
|
|
msg_id = await send_status_message(context, message.chat_id, status_text)
|
|||
|
|
if msg_id is not None:
|
|||
|
|
status_message_ids.append(msg_id)
|
|||
|
|
|
|||
|
|
# Добавляем задачу в очередь
|
|||
|
|
await queue_manager.add_task(
|
|||
|
|
user_id=user.id,
|
|||
|
|
username=user.username,
|
|||
|
|
url=text,
|
|||
|
|
chat_id=message.chat_id,
|
|||
|
|
message_id=message.message_id,
|
|||
|
|
callback=status_callback,
|
|||
|
|
status_message_ids=status_message_ids
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _split_audio_to_parts(file_path: Path, max_part_mb: int, bitrate_kbps: int) -> list[Path]:
|
|||
|
|
"""Разбить аудио на части (перекодирование в CBR для стабильного размера)."""
|
|||
|
|
parts_dir = file_path.parent / f"parts_{file_path.stem}"
|
|||
|
|
parts_dir.mkdir(parents=True, exist_ok=True)
|
|||
|
|
part_pattern = parts_dir / f"{file_path.stem}_part%03d.mp3"
|
|||
|
|
|
|||
|
|
max_bytes = max_part_mb * 1024 * 1024
|
|||
|
|
segment_time = max(60, int((max_bytes * 8) / (bitrate_kbps * 1000))) # минимум 60 секунд
|
|||
|
|
|
|||
|
|
cmd = [
|
|||
|
|
'ffmpeg',
|
|||
|
|
'-i', str(file_path),
|
|||
|
|
'-b:a', f'{bitrate_kbps}k',
|
|||
|
|
'-f', 'segment',
|
|||
|
|
'-segment_time', str(segment_time),
|
|||
|
|
'-reset_timestamps', '1',
|
|||
|
|
'-y',
|
|||
|
|
str(part_pattern),
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
process = await asyncio.create_subprocess_exec(
|
|||
|
|
*cmd,
|
|||
|
|
stdout=asyncio.subprocess.PIPE,
|
|||
|
|
stderr=asyncio.subprocess.PIPE
|
|||
|
|
)
|
|||
|
|
stdout, stderr = await process.communicate()
|
|||
|
|
if process.returncode != 0:
|
|||
|
|
error = stderr.decode('utf-8', errors='ignore')
|
|||
|
|
raise Exception(f"Ошибка разбиения на части: {error[:200]}")
|
|||
|
|
|
|||
|
|
prefix = f"{file_path.stem}_part"
|
|||
|
|
parts = sorted(
|
|||
|
|
p for p in parts_dir.iterdir()
|
|||
|
|
if p.is_file() and p.name.startswith(prefix) and p.suffix == ".mp3"
|
|||
|
|
)
|
|||
|
|
logger.info(f"Split into {len(parts)} parts in {parts_dir}")
|
|||
|
|
return parts
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def send_file_to_user(bot, chat_id: int, file_path: str, filename: str, config: Config):
|
|||
|
|
"""Отправить MP3 файл пользователю. Если файл большой — отправить частями."""
|
|||
|
|
try:
|
|||
|
|
path = Path(file_path)
|
|||
|
|
max_bytes = config.max_part_mb * 1024 * 1024
|
|||
|
|
|
|||
|
|
if path.stat().st_size > max_bytes:
|
|||
|
|
parts = await _split_audio_to_parts(path, config.max_part_mb, config.audio_bitrate_kbps)
|
|||
|
|
total = len(parts)
|
|||
|
|
if total == 0:
|
|||
|
|
raise Exception("Не удалось разбить файл на части")
|
|||
|
|
for idx, part in enumerate(parts, start=1):
|
|||
|
|
part_name = f"{idx:02d}_{path.stem}.mp3"
|
|||
|
|
with open(part, 'rb') as f:
|
|||
|
|
await bot.send_document(
|
|||
|
|
chat_id=chat_id,
|
|||
|
|
document=f,
|
|||
|
|
filename=part_name
|
|||
|
|
)
|
|||
|
|
logger.info(f"Sent part {idx}/{total} to user {chat_id}")
|
|||
|
|
# Очистка частей
|
|||
|
|
for part in parts:
|
|||
|
|
part.unlink(missing_ok=True)
|
|||
|
|
parts_dir = parts[0].parent if parts else None
|
|||
|
|
if parts_dir:
|
|||
|
|
try:
|
|||
|
|
parts_dir.rmdir()
|
|||
|
|
except OSError:
|
|||
|
|
logger.warning(f"Parts dir not empty: {parts_dir}")
|
|||
|
|
else:
|
|||
|
|
with open(path, 'rb') as f:
|
|||
|
|
await bot.send_document(
|
|||
|
|
chat_id=chat_id,
|
|||
|
|
document=f,
|
|||
|
|
filename=filename
|
|||
|
|
)
|
|||
|
|
logger.info(f"Sent file {filename} to user {chat_id}")
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"Error sending file to user: {e}", exc_info=True)
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def send_to_admin_bot(
|
|||
|
|
admin_bot_app: Application,
|
|||
|
|
admin_manager: AdminManager,
|
|||
|
|
config: Config,
|
|||
|
|
title: str,
|
|||
|
|
username: Optional[str],
|
|||
|
|
user_id: int,
|
|||
|
|
url: str,
|
|||
|
|
file_path: str
|
|||
|
|
):
|
|||
|
|
"""Отправить уведомление и файл в admin-bot."""
|
|||
|
|
try:
|
|||
|
|
# Формируем сообщение
|
|||
|
|
requested_by = f"@{username}" if username else f"user_id: {user_id}"
|
|||
|
|
message_text = f"title: {title}\nrequested_by: {requested_by}\nurl: {url}"
|
|||
|
|
|
|||
|
|
# Определяем получателей
|
|||
|
|
if config.admin_chat_id:
|
|||
|
|
recipients = [int(config.admin_chat_id)]
|
|||
|
|
else:
|
|||
|
|
recipients = admin_manager.get_all_admins()
|
|||
|
|
|
|||
|
|
if not recipients:
|
|||
|
|
logger.warning("No admin recipients found")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# Отправляем всем получателям
|
|||
|
|
filename = Path(file_path).name
|
|||
|
|
for chat_id in recipients:
|
|||
|
|
try:
|
|||
|
|
# Отправляем текст
|
|||
|
|
await admin_bot_app.bot.send_message(
|
|||
|
|
chat_id=chat_id,
|
|||
|
|
text=message_text
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Отправляем файл (если большой — частями)
|
|||
|
|
path = Path(file_path)
|
|||
|
|
max_bytes = config.max_part_mb * 1024 * 1024
|
|||
|
|
if path.stat().st_size > max_bytes:
|
|||
|
|
parts = await _split_audio_to_parts(path, config.max_part_mb, config.audio_bitrate_kbps)
|
|||
|
|
total = len(parts)
|
|||
|
|
for idx, part in enumerate(parts, start=1):
|
|||
|
|
part_name = f"{idx:02d}_{path.stem}.mp3"
|
|||
|
|
with open(part, 'rb') as f:
|
|||
|
|
await admin_bot_app.bot.send_document(
|
|||
|
|
chat_id=chat_id,
|
|||
|
|
document=f,
|
|||
|
|
filename=part_name
|
|||
|
|
)
|
|||
|
|
for part in parts:
|
|||
|
|
part.unlink(missing_ok=True)
|
|||
|
|
parts_dir = parts[0].parent if parts else None
|
|||
|
|
if parts_dir:
|
|||
|
|
parts_dir.rmdir()
|
|||
|
|
else:
|
|||
|
|
with open(file_path, 'rb') as f:
|
|||
|
|
await admin_bot_app.bot.send_document(
|
|||
|
|
chat_id=chat_id,
|
|||
|
|
document=f,
|
|||
|
|
filename=filename
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
logger.info(f"Sent notification and file to admin {chat_id}")
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"Error sending to admin {chat_id}: {e}")
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"Error in send_to_admin_bot: {e}", exc_info=True)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def create_user_bot_application(
|
|||
|
|
config: Config,
|
|||
|
|
queue_manager: QueueManager,
|
|||
|
|
admin_manager: AdminManager,
|
|||
|
|
admin_bot_app: Application,
|
|||
|
|
statistics: Statistics
|
|||
|
|
) -> Application:
|
|||
|
|
"""Создать и настроить приложение user-bot."""
|
|||
|
|
|
|||
|
|
# Создаём приложение с увеличенными таймаутами для отправки файлов
|
|||
|
|
request = HTTPXRequest(
|
|||
|
|
connect_timeout=config.tg_connect_timeout,
|
|||
|
|
read_timeout=config.tg_read_timeout,
|
|||
|
|
write_timeout=config.tg_write_timeout,
|
|||
|
|
pool_timeout=config.tg_pool_timeout,
|
|||
|
|
)
|
|||
|
|
app = Application.builder().token(config.user_bot_token).request(request).build()
|
|||
|
|
|
|||
|
|
# Сохраняем в bot_data для доступа из handlers
|
|||
|
|
app.bot_data['config'] = config
|
|||
|
|
app.bot_data['queue_manager'] = queue_manager
|
|||
|
|
app.bot_data['admin_manager'] = admin_manager
|
|||
|
|
app.bot_data['admin_bot_app'] = admin_bot_app
|
|||
|
|
app.bot_data['statistics'] = statistics
|
|||
|
|
|
|||
|
|
# Обработчик команды /start
|
|||
|
|
app.add_handler(CommandHandler("start", start_command))
|
|||
|
|
|
|||
|
|
# Обработчик сообщений (сначала проверяем на ожидание имени файла, потом на ссылку)
|
|||
|
|
message_handler = MessageHandler(
|
|||
|
|
filters.TEXT & ~filters.COMMAND,
|
|||
|
|
handle_filename_response # Этот handler проверяет оба случая
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
app.add_handler(message_handler)
|
|||
|
|
|
|||
|
|
return app
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def worker_function(task: Task, config: Config, admin_manager: AdminManager, admin_bot_app: Application, statistics: Statistics, user_bot_app: Optional[Application]) -> str:
|
|||
|
|
"""Функция-воркер для обработки задачи."""
|
|||
|
|
mp3_path = None
|
|||
|
|
try:
|
|||
|
|
# Обрабатываем задачу
|
|||
|
|
mp3_path = await process_task(task, config, admin_manager, admin_bot_app)
|
|||
|
|
|
|||
|
|
# Получаем название файла
|
|||
|
|
filename = Path(mp3_path).name
|
|||
|
|
|
|||
|
|
# Отправляем файл пользователю
|
|||
|
|
if user_bot_app:
|
|||
|
|
await send_file_to_user(
|
|||
|
|
user_bot_app.bot,
|
|||
|
|
task.chat_id,
|
|||
|
|
mp3_path,
|
|||
|
|
filename,
|
|||
|
|
config
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
logger.warning("user_bot_app is not available; skipping send to user")
|
|||
|
|
|
|||
|
|
# Отправляем в admin-bot
|
|||
|
|
await send_to_admin_bot(
|
|||
|
|
admin_bot_app,
|
|||
|
|
admin_manager,
|
|||
|
|
config,
|
|||
|
|
filename.replace('.mp3', ''),
|
|||
|
|
task.username,
|
|||
|
|
task.user_id,
|
|||
|
|
task.url,
|
|||
|
|
mp3_path
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Увеличиваем счётчик обработанных ссылок
|
|||
|
|
statistics.increment_processed_urls()
|
|||
|
|
|
|||
|
|
# Удаляем статусные сообщения после завершения
|
|||
|
|
if user_bot_app and task.status_message_ids:
|
|||
|
|
for msg_id in task.status_message_ids:
|
|||
|
|
try:
|
|||
|
|
await user_bot_app.bot.delete_message(chat_id=task.chat_id, message_id=msg_id)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"Failed to delete status message {msg_id}: {e}")
|
|||
|
|
|
|||
|
|
return mp3_path
|
|||
|
|
|
|||
|
|
finally:
|
|||
|
|
# Удаляем временный файл
|
|||
|
|
if mp3_path:
|
|||
|
|
try:
|
|||
|
|
Path(mp3_path).unlink(missing_ok=True)
|
|||
|
|
logger.info(f"Deleted temporary file: {mp3_path}")
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"Error deleting file {mp3_path}: {e}")
|