init commit
This commit is contained in:
10
bot/download_audio_services/__init__.py
Normal file
10
bot/download_audio_services/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from .base import Downloader
|
||||
from .youtube import YouTubeDownloader
|
||||
|
||||
def get_downloader(service_name: str) -> Downloader:
|
||||
"""Фабрика загрузчиков"""
|
||||
downloaders = {
|
||||
"youtube": YouTubeDownloader,
|
||||
# Здесь можно регистрировать новые загрузчики
|
||||
}
|
||||
return downloaders.get(service_name.lower())
|
||||
23
bot/download_audio_services/base.py
Normal file
23
bot/download_audio_services/base.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import abc
|
||||
import os
|
||||
import re
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
class Downloader(abc.ABC):
|
||||
"""Базовый класс для загрузчиков"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def is_supported_url(self, url: str) -> bool:
|
||||
"""Проверяет, поддерживается ли URL"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def download_audio(self, url: str, config: dict) -> Tuple[Optional[str], Optional[str], Optional[int]]:
|
||||
"""Скачивает аудио и возвращает (путь к файлу, название, длительность)"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def sanitize_filename(name: str) -> str:
|
||||
"""Очищает название файла от недопустимых символов"""
|
||||
return re.sub(r'[^\w\-_\. ]', '', name)[:100]
|
||||
62
bot/download_audio_services/youtube.py
Normal file
62
bot/download_audio_services/youtube.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import asyncio
|
||||
import os
|
||||
import uuid
|
||||
import yt_dlp
|
||||
from typing import Optional, Tuple
|
||||
from .base import Downloader
|
||||
from bot.utils import run_blocking
|
||||
|
||||
|
||||
class YouTubeDownloader(Downloader):
|
||||
"""Загрузчик для YouTube"""
|
||||
|
||||
def is_supported_url(self, url: str) -> bool:
|
||||
return "youtube.com" in url or "youtu.be" in url
|
||||
|
||||
async def download_audio(self, url: str, config: dict) -> Tuple[Optional[str], Optional[str], Optional[int]]:
|
||||
"""Скачивает аудио с YouTube"""
|
||||
# Настройки yt-dlp
|
||||
ydl_opts = {
|
||||
'format': 'bestaudio/best',
|
||||
'outtmpl': os.path.join(config['tmp_dir'], f'%(id)s.%(ext)s'),
|
||||
'postprocessors': [{
|
||||
'key': 'FFmpegExtractAudio',
|
||||
'preferredcodec': 'mp3',
|
||||
'preferredquality': str(config.get('audio_quality', 192)),
|
||||
}],
|
||||
'quiet': True,
|
||||
'no_warnings': True,
|
||||
'proxy': config.get('proxy'),
|
||||
'max_filesize': 50 * 1024 * 1024, # 100MB
|
||||
'noplaylist': True,
|
||||
}
|
||||
|
||||
# Ограничение длительности
|
||||
if max_duration := config.get('max_duration'):
|
||||
ydl_opts['match_filter'] = yt_dlp.match_filter_func(
|
||||
f"duration < {max_duration}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Запускаем в отдельном процессе
|
||||
result = await run_blocking(self._download, url, ydl_opts)
|
||||
return result
|
||||
except Exception as e:
|
||||
return None, str(e)
|
||||
|
||||
def _download(self, url: str, opts: dict) -> Tuple[str, str, int]:
|
||||
"""Синхронная загрузка (выполняется в отдельном процессе)"""
|
||||
with yt_dlp.YoutubeDL(opts) as ydl:
|
||||
info = ydl.extract_info(url, download=True)
|
||||
filename = ydl.prepare_filename(info)
|
||||
base, _ = os.path.splitext(filename)
|
||||
mp3_path = base + '.mp3'
|
||||
|
||||
# Получаем название
|
||||
title = info.get('title', 'audio') or 'audio'
|
||||
clean_title = self.sanitize_filename(title)
|
||||
|
||||
# Получаем длительность в миллисекундах
|
||||
duration_ms = int(info.get('duration', 0) * 1000)
|
||||
|
||||
return mp3_path, clean_title, duration_ms
|
||||
Reference in New Issue
Block a user