"""资源代理辅助方法与路由装饰器。""" from collections.abc import Awaitable, Callable from functools import wraps import re from typing import Any from app.config import settings from fastapi import Response from pydantic import BaseModel Handler = Callable[..., Awaitable[Any]] def _replace_asset_urls_in_string(value: str) -> str: result = value custom_domain = settings.custom_asset_domain asset_prefix = settings.asset_proxy_prefix avatar_prefix = settings.avatar_proxy_prefix beatmap_prefix = settings.beatmap_proxy_prefix audio_proxy_base_url = f"{settings.server_url}api/private/audio/beatmapset" result = re.sub( r"^https://assets\.ppy\.sh/", f"https://{asset_prefix}.{custom_domain}/", result, ) result = re.sub( r"^https://b\.ppy\.sh/preview/(\d+)\\.mp3", rf"{audio_proxy_base_url}/\1", result, ) result = re.sub( r"^//b\.ppy\.sh/preview/(\d+)\\.mp3", rf"{audio_proxy_base_url}/\1", result, ) result = re.sub( r"^https://a\.ppy\.sh/", f"https://{avatar_prefix}.{custom_domain}/", result, ) result = re.sub( r"https://b\.ppy\.sh/", f"https://{beatmap_prefix}.{custom_domain}/", result, ) return result def _replace_asset_urls_in_data(data: Any) -> Any: if isinstance(data, str): return _replace_asset_urls_in_string(data) if isinstance(data, list): return [_replace_asset_urls_in_data(item) for item in data] if isinstance(data, tuple): return tuple(_replace_asset_urls_in_data(item) for item in data) if isinstance(data, dict): return {key: _replace_asset_urls_in_data(value) for key, value in data.items()} return data async def replace_asset_urls(data: Any) -> Any: """替换数据中的 osu! 资源 URL。""" if not settings.enable_asset_proxy: return data if hasattr(data, "model_dump"): raw = data.model_dump() processed = _replace_asset_urls_in_data(raw) try: return data.__class__(**processed) except Exception: return processed if isinstance(data, (dict, list, tuple, str)): return _replace_asset_urls_in_data(data) return data def asset_proxy_response(func: Handler) -> Handler: """装饰器:在返回响应前替换资源 URL。""" @wraps(func) async def wrapper(*args, **kwargs): result = await func(*args, **kwargs) if not settings.enable_asset_proxy: return result if isinstance(result, Response): return result if isinstance(result, BaseModel): result = result.model_dump() return _replace_asset_urls_in_data(result) return wrapper # type: ignore[return-value]