131 lines
5.8 KiB
Python
131 lines
5.8 KiB
Python
"""
|
||
音频代理服务
|
||
提供从osu!官方获取beatmapset音频预览并缓存的功能
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from app.log import logger
|
||
|
||
from fastapi import HTTPException
|
||
import httpx
|
||
import redis.asyncio as redis
|
||
|
||
|
||
class AudioProxyService:
|
||
"""音频代理服务"""
|
||
|
||
def __init__(self, redis_binary_client: redis.Redis, redis_text_client: redis.Redis):
|
||
self.redis_binary = redis_binary_client
|
||
self.redis_text = redis_text_client
|
||
self.http_client = httpx.AsyncClient(timeout=30.0)
|
||
self._cache_ttl = 7 * 24 * 60 * 60
|
||
|
||
async def close(self):
|
||
"""关闭HTTP客户端"""
|
||
await self.http_client.aclose()
|
||
|
||
def _get_beatmapset_cache_key(self, beatmapset_id: int) -> str:
|
||
"""生成beatmapset音频缓存键"""
|
||
return f"beatmapset_audio:{beatmapset_id}"
|
||
|
||
def _get_beatmapset_metadata_key(self, beatmapset_id: int) -> str:
|
||
"""生成beatmapset音频元数据缓存键"""
|
||
return f"beatmapset_audio_meta:{beatmapset_id}"
|
||
|
||
async def get_beatmapset_audio_from_cache(self, beatmapset_id: int) -> tuple[bytes, str] | None:
|
||
"""从缓存获取beatmapset音频数据和内容类型"""
|
||
try:
|
||
cache_key = self._get_beatmapset_cache_key(beatmapset_id)
|
||
metadata_key = self._get_beatmapset_metadata_key(beatmapset_id)
|
||
|
||
# 获取音频数据(二进制)和元数据(文本)
|
||
audio_data = await self.redis_binary.get(cache_key)
|
||
metadata = await self.redis_text.get(metadata_key)
|
||
|
||
if audio_data and metadata:
|
||
logger.debug(f"Beatmapset audio cache hit for ID: {beatmapset_id}")
|
||
# audio_data 已经是 bytes 类型,metadata 是 str 类型
|
||
return audio_data, metadata
|
||
return None
|
||
except (redis.RedisError, redis.ConnectionError) as e:
|
||
logger.error(f"Error getting beatmapset audio from cache: {e}")
|
||
return None
|
||
|
||
async def cache_beatmapset_audio(self, beatmapset_id: int, audio_data: bytes, content_type: str):
|
||
"""缓存beatmapset音频数据"""
|
||
try:
|
||
cache_key = self._get_beatmapset_cache_key(beatmapset_id)
|
||
metadata_key = self._get_beatmapset_metadata_key(beatmapset_id)
|
||
|
||
# 缓存音频数据(二进制)和元数据(文本)
|
||
await self.redis_binary.setex(cache_key, self._cache_ttl, audio_data)
|
||
await self.redis_text.setex(metadata_key, self._cache_ttl, content_type)
|
||
|
||
logger.debug(f"Cached beatmapset audio for ID: {beatmapset_id}, size: {len(audio_data)} bytes")
|
||
except (redis.RedisError, redis.ConnectionError) as e:
|
||
logger.error(f"Error caching beatmapset audio: {e}")
|
||
|
||
async def fetch_beatmapset_audio(self, beatmapset_id: int) -> tuple[bytes, str]:
|
||
"""从osu!官方获取beatmapset音频预览"""
|
||
try:
|
||
# 构建 osu! 官方预览音频 URL
|
||
preview_url = f"https://b.ppy.sh/preview/{beatmapset_id}.mp3"
|
||
logger.info(f"Fetching beatmapset audio from: {preview_url}")
|
||
|
||
response = await self.http_client.get(preview_url)
|
||
response.raise_for_status()
|
||
|
||
# osu!预览音频通常为mp3格式
|
||
content_type = response.headers.get("content-type", "audio/mpeg")
|
||
audio_data = response.content
|
||
|
||
# 检查文件大小限制(10MB,预览音频通常不会太大)
|
||
max_size = 10 * 1024 * 1024 # 10MB
|
||
if len(audio_data) > max_size:
|
||
raise HTTPException(
|
||
status_code=413, detail=f"Audio file too large: {len(audio_data)} bytes (max: {max_size})"
|
||
)
|
||
|
||
if len(audio_data) == 0:
|
||
raise HTTPException(status_code=404, detail="Audio preview not available for this beatmapset")
|
||
|
||
logger.info(f"Successfully fetched beatmapset audio: {len(audio_data)} bytes, type: {content_type}")
|
||
return audio_data, content_type
|
||
|
||
except httpx.HTTPStatusError as e:
|
||
logger.error(f"HTTP error fetching beatmapset audio for ID {beatmapset_id}: {e}")
|
||
if e.response.status_code == 404:
|
||
raise HTTPException(status_code=404, detail="Audio preview not found for this beatmapset") from e
|
||
else:
|
||
raise HTTPException(
|
||
status_code=e.response.status_code, detail=f"Failed to fetch audio: {e.response.status_code}"
|
||
) from e
|
||
except httpx.RequestError as e:
|
||
logger.error(f"Request error fetching beatmapset audio for ID {beatmapset_id}: {e}")
|
||
raise HTTPException(status_code=503, detail="Failed to connect to osu! servers") from e
|
||
except Exception as e:
|
||
logger.error(f"Unexpected error fetching beatmapset audio for ID {beatmapset_id}: {e}")
|
||
raise HTTPException(status_code=500, detail="Internal server error while fetching audio") from e
|
||
|
||
async def get_beatmapset_audio(self, beatmapset_id: int) -> tuple[bytes, str]:
|
||
"""根据 beatmapset_id 获取音频预览"""
|
||
# 先尝试从缓存获取
|
||
cached_result = await self.get_beatmapset_audio_from_cache(beatmapset_id)
|
||
if cached_result:
|
||
return cached_result
|
||
|
||
# 缓存未命中,从osu!官方获取
|
||
audio_data, content_type = await self.fetch_beatmapset_audio(beatmapset_id)
|
||
|
||
# 缓存新获取的音频数据
|
||
await self.cache_beatmapset_audio(beatmapset_id, audio_data, content_type)
|
||
|
||
return audio_data, content_type
|
||
|
||
|
||
def get_audio_proxy_service(redis_binary_client: redis.Redis, redis_text_client: redis.Redis) -> AudioProxyService:
|
||
"""获取音频代理服务实例"""
|
||
# 每次创建新实例,避免全局状态
|
||
return AudioProxyService(redis_binary_client, redis_text_client)
|