From 046f89440708da3bf62aa82a2cc5cea6872d6829 Mon Sep 17 00:00:00 2001 From: MingxuanGame Date: Fri, 3 Oct 2025 17:12:28 +0000 Subject: [PATCH] refactor(assets_proxy): use decorators to simplify code --- app/helpers/asset_proxy_helper.py | 108 +++++++++++++++++++++++++++ app/models/beatmap.py | 3 + app/router/__init__.py | 2 - app/router/auth.py | 2 - app/router/fetcher.py | 2 - app/router/file.py | 2 - app/router/lio.py | 2 - app/router/notification/__init__.py | 2 - app/router/notification/banchobot.py | 2 - app/router/notification/channel.py | 2 - app/router/notification/message.py | 2 - app/router/notification/server.py | 2 - app/router/private/__init__.py | 2 - app/router/private/admin.py | 2 - app/router/private/audio_proxy.py | 2 - app/router/private/avatar.py | 2 - app/router/private/beatmapset.py | 2 - app/router/private/cover.py | 2 - app/router/private/oauth.py | 2 - app/router/private/relationship.py | 2 - app/router/private/router.py | 2 - app/router/private/score.py | 2 - app/router/private/team.py | 2 - app/router/private/totp.py | 2 - app/router/private/username.py | 2 - app/router/redirect.py | 2 - app/router/v1/__init__.py | 2 - app/router/v1/beatmap.py | 2 - app/router/v1/public_router.py | 2 - app/router/v1/public_user.py | 2 - app/router/v1/replay.py | 2 - app/router/v1/router.py | 2 - app/router/v1/score.py | 2 - app/router/v1/user.py | 2 - app/router/v2/__init__.py | 2 - app/router/v2/beatmap.py | 2 - app/router/v2/beatmapset.py | 36 +++------ app/router/v2/cache.py | 2 - app/router/v2/me.py | 2 - app/router/v2/misc.py | 2 - app/router/v2/ranking.py | 2 - app/router/v2/relationship.py | 2 - app/router/v2/room.py | 2 - app/router/v2/router.py | 2 - app/router/v2/score.py | 2 - app/router/v2/session_verify.py | 2 - app/router/v2/tags.py | 2 - app/router/v2/user.py | 51 ++++++------- app/service/asset_proxy_helper.py | 79 -------------------- app/service/asset_proxy_service.py | 83 -------------------- app/service/ranking_cache_service.py | 9 +-- app/service/user_cache_service.py | 5 +- pyproject.toml | 2 +- 53 files changed, 151 insertions(+), 313 deletions(-) create mode 100644 app/helpers/asset_proxy_helper.py delete mode 100644 app/service/asset_proxy_helper.py delete mode 100644 app/service/asset_proxy_service.py diff --git a/app/helpers/asset_proxy_helper.py b/app/helpers/asset_proxy_helper.py new file mode 100644 index 0000000..87c68ab --- /dev/null +++ b/app/helpers/asset_proxy_helper.py @@ -0,0 +1,108 @@ +"""资源代理辅助方法与路由装饰器。""" + +from __future__ import annotations + +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] diff --git a/app/models/beatmap.py b/app/models/beatmap.py index 718041f..c4e71b1 100644 --- a/app/models/beatmap.py +++ b/app/models/beatmap.py @@ -204,3 +204,6 @@ class SearchQueryModel(BaseModel): default=None, description="游标字符串,用于分页", ) + + +SearchQueryModel.model_rebuild() diff --git a/app/router/__init__.py b/app/router/__init__.py index fc5d2a7..814d19b 100644 --- a/app/router/__init__.py +++ b/app/router/__init__.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from .auth import router as auth_router from .fetcher import fetcher_router as fetcher_router from .file import file_router as file_router diff --git a/app/router/auth.py b/app/router/auth.py index 1512642..b51028d 100644 --- a/app/router/auth.py +++ b/app/router/auth.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from datetime import timedelta import re from typing import Annotated, Literal diff --git a/app/router/fetcher.py b/app/router/fetcher.py index 887eabf..23bf8e6 100644 --- a/app/router/fetcher.py +++ b/app/router/fetcher.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from app.dependencies.fetcher import Fetcher from fastapi import APIRouter diff --git a/app/router/file.py b/app/router/file.py index 14263f9..184baff 100644 --- a/app/router/file.py +++ b/app/router/file.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from app.dependencies.storage import StorageService as StorageServiceDep from app.storage import LocalStorageService diff --git a/app/router/lio.py b/app/router/lio.py index 93e0088..096a883 100644 --- a/app/router/lio.py +++ b/app/router/lio.py @@ -1,7 +1,5 @@ """LIO (Legacy IO) router for osu-server-spectator compatibility.""" -from __future__ import annotations - import base64 import json from typing import Any diff --git a/app/router/notification/__init__.py b/app/router/notification/__init__.py index 176fbdb..b9b0658 100644 --- a/app/router/notification/__init__.py +++ b/app/router/notification/__init__.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from app.config import settings from app.database.notification import Notification, UserNotification from app.database.user import User diff --git a/app/router/notification/banchobot.py b/app/router/notification/banchobot.py index a491b7d..39a01c5 100644 --- a/app/router/notification/banchobot.py +++ b/app/router/notification/banchobot.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import asyncio from collections.abc import Awaitable, Callable from math import ceil diff --git a/app/router/notification/channel.py b/app/router/notification/channel.py index 62861ec..324c0ff 100644 --- a/app/router/notification/channel.py +++ b/app/router/notification/channel.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Annotated, Any, Literal, Self from app.database.chat import ( diff --git a/app/router/notification/message.py b/app/router/notification/message.py index 8dec1cb..34ebc48 100644 --- a/app/router/notification/message.py +++ b/app/router/notification/message.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Annotated from app.database import ChatMessageResp diff --git a/app/router/notification/server.py b/app/router/notification/server.py index 0732124..021a690 100644 --- a/app/router/notification/server.py +++ b/app/router/notification/server.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import asyncio from typing import Annotated, overload diff --git a/app/router/private/__init__.py b/app/router/private/__init__.py index f1715a1..7664822 100644 --- a/app/router/private/__init__.py +++ b/app/router/private/__init__.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from app.config import settings from . import admin, audio_proxy, avatar, beatmapset, cover, oauth, relationship, score, team, username # noqa: F401 diff --git a/app/router/private/admin.py b/app/router/private/admin.py index 57dcf98..c9bf1d4 100644 --- a/app/router/private/admin.py +++ b/app/router/private/admin.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Annotated from app.database.auth import OAuthToken diff --git a/app/router/private/audio_proxy.py b/app/router/private/audio_proxy.py index ca8cda4..da5d946 100644 --- a/app/router/private/audio_proxy.py +++ b/app/router/private/audio_proxy.py @@ -3,8 +3,6 @@ 提供从osu!官方获取beatmapset音频预览的代理服务 """ -from __future__ import annotations - from typing import Annotated from app.dependencies.database import get_redis, get_redis_binary diff --git a/app/router/private/avatar.py b/app/router/private/avatar.py index 0af8694..dfe8166 100644 --- a/app/router/private/avatar.py +++ b/app/router/private/avatar.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import hashlib from typing import Annotated diff --git a/app/router/private/beatmapset.py b/app/router/private/beatmapset.py index 5b80841..f206562 100644 --- a/app/router/private/beatmapset.py +++ b/app/router/private/beatmapset.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Annotated from app.database.beatmap import Beatmap diff --git a/app/router/private/cover.py b/app/router/private/cover.py index 71992e0..b0a5b53 100644 --- a/app/router/private/cover.py +++ b/app/router/private/cover.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import hashlib from typing import Annotated diff --git a/app/router/private/oauth.py b/app/router/private/oauth.py index 2af00dc..8c4c664 100644 --- a/app/router/private/oauth.py +++ b/app/router/private/oauth.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import secrets from typing import Annotated diff --git a/app/router/private/relationship.py b/app/router/private/relationship.py index 1f882cb..7b051a8 100644 --- a/app/router/private/relationship.py +++ b/app/router/private/relationship.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Annotated from app.database import Relationship diff --git a/app/router/private/router.py b/app/router/private/router.py index 6882bc9..6f5fac4 100644 --- a/app/router/private/router.py +++ b/app/router/private/router.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from app.dependencies.rate_limit import LIMITERS from fastapi import APIRouter diff --git a/app/router/private/score.py b/app/router/private/score.py index e640121..c5a6a97 100644 --- a/app/router/private/score.py +++ b/app/router/private/score.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from app.database.score import Score from app.dependencies.database import Database, Redis from app.dependencies.storage import StorageService diff --git a/app/router/private/team.py b/app/router/private/team.py index b60461c..2cf6cae 100644 --- a/app/router/private/team.py +++ b/app/router/private/team.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import hashlib from typing import Annotated diff --git a/app/router/private/totp.py b/app/router/private/totp.py index 06406aa..780c1ec 100644 --- a/app/router/private/totp.py +++ b/app/router/private/totp.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Annotated from app.auth import ( diff --git a/app/router/private/username.py b/app/router/private/username.py index 18eb219..10c5411 100644 --- a/app/router/private/username.py +++ b/app/router/private/username.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Annotated from app.auth import validate_username diff --git a/app/router/redirect.py b/app/router/redirect.py index bec9eca..f90a5d4 100644 --- a/app/router/redirect.py +++ b/app/router/redirect.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import urllib.parse from app.config import settings diff --git a/app/router/v1/__init__.py b/app/router/v1/__init__.py index 4a8ca41..f2ae0aa 100644 --- a/app/router/v1/__init__.py +++ b/app/router/v1/__init__.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from . import beatmap, public_user, replay, score, user # noqa: F401 from .public_router import public_router as api_v1_public_router from .router import router as api_v1_router diff --git a/app/router/v1/beatmap.py b/app/router/v1/beatmap.py index 6ca3775..1df2048 100644 --- a/app/router/v1/beatmap.py +++ b/app/router/v1/beatmap.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from datetime import datetime from typing import Annotated, Literal diff --git a/app/router/v1/public_router.py b/app/router/v1/public_router.py index 4d2c240..596beec 100644 --- a/app/router/v1/public_router.py +++ b/app/router/v1/public_router.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from datetime import datetime from enum import Enum diff --git a/app/router/v1/public_user.py b/app/router/v1/public_user.py index dadbfb9..6ca98d8 100644 --- a/app/router/v1/public_user.py +++ b/app/router/v1/public_user.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Annotated, Literal from app.database.statistics import UserStatistics diff --git a/app/router/v1/replay.py b/app/router/v1/replay.py index b0ffc99..0b16b3a 100644 --- a/app/router/v1/replay.py +++ b/app/router/v1/replay.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import base64 from datetime import date from typing import Annotated, Literal diff --git a/app/router/v1/router.py b/app/router/v1/router.py index f46ea2b..612d222 100644 --- a/app/router/v1/router.py +++ b/app/router/v1/router.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from datetime import datetime from enum import Enum diff --git a/app/router/v1/score.py b/app/router/v1/score.py index 4ac9b42..f880846 100644 --- a/app/router/v1/score.py +++ b/app/router/v1/score.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from datetime import datetime, timedelta from typing import Annotated, Literal diff --git a/app/router/v1/user.py b/app/router/v1/user.py index 52ee19a..f34ae76 100644 --- a/app/router/v1/user.py +++ b/app/router/v1/user.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from datetime import datetime from typing import Annotated, Literal diff --git a/app/router/v2/__init__.py b/app/router/v2/__init__.py index 0bd06cd..e12f56b 100644 --- a/app/router/v2/__init__.py +++ b/app/router/v2/__init__.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from . import ( # noqa: F401 beatmap, beatmapset, diff --git a/app/router/v2/beatmap.py b/app/router/v2/beatmap.py index 152e8f0..e389865 100644 --- a/app/router/v2/beatmap.py +++ b/app/router/v2/beatmap.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import asyncio import hashlib import json diff --git a/app/router/v2/beatmapset.py b/app/router/v2/beatmapset.py index 3cfcdb1..d61b5f8 100644 --- a/app/router/v2/beatmapset.py +++ b/app/router/v2/beatmapset.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import re from typing import Annotated, Literal from urllib.parse import parse_qs @@ -12,8 +10,8 @@ from app.dependencies.database import Database, Redis, with_db from app.dependencies.fetcher import Fetcher from app.dependencies.geoip import IPAddress, get_geoip_helper from app.dependencies.user import ClientUser, get_current_user +from app.helpers.asset_proxy_helper import asset_proxy_response from app.models.beatmap import SearchQueryModel -from app.service.asset_proxy_helper import process_response_assets from app.service.beatmapset_cache_service import generate_hash from .router import router @@ -45,8 +43,9 @@ async def _save_to_db(sets: SearchBeatmapsetsResp): tags=["谱面集"], response_model=SearchBeatmapsetsResp, ) +@asset_proxy_response async def search_beatmapset( - query: Annotated[SearchQueryModel, Query(...)], + query: Annotated[SearchQueryModel, Query()], request: Request, background_tasks: BackgroundTasks, current_user: Annotated[User, Security(get_current_user, scopes=["public"])], @@ -102,9 +101,7 @@ async def search_beatmapset( cached_result = await cache_service.get_search_from_cache(query_hash, cursor_hash) if cached_result: sets = SearchBeatmapsetsResp(**cached_result) - # 处理资源代理 - processed_sets = await process_response_assets(sets) - return processed_sets + return sets try: sets = await fetcher.search_beatmapset(query, cursor, redis) @@ -112,10 +109,7 @@ async def search_beatmapset( # 缓存搜索结果 await cache_service.cache_search_result(query_hash, cursor_hash, sets.model_dump()) - - # 处理资源代理 - processed_sets = await process_response_assets(sets) - return processed_sets + return sets except HTTPError as e: raise HTTPException(status_code=500, detail=str(e)) from e @@ -127,6 +121,7 @@ async def search_beatmapset( response_model=BeatmapsetResp, description=("通过谱面 ID 查询所属谱面集。"), ) +@asset_proxy_response async def lookup_beatmapset( db: Database, request: Request, @@ -138,9 +133,7 @@ async def lookup_beatmapset( # 先尝试从缓存获取 cached_resp = await cache_service.get_beatmap_lookup_from_cache(beatmap_id) if cached_resp: - # 处理资源代理 - processed_resp = await process_response_assets(cached_resp) - return processed_resp + return cached_resp try: beatmap = await Beatmap.get_or_fetch(db, fetcher, bid=beatmap_id) @@ -148,10 +141,7 @@ async def lookup_beatmapset( # 缓存结果 await cache_service.cache_beatmap_lookup(beatmap_id, resp) - - # 处理资源代理 - processed_resp = await process_response_assets(resp) - return processed_resp + return resp except HTTPError as exc: raise HTTPException(status_code=404, detail="Beatmap not found") from exc @@ -163,6 +153,7 @@ async def lookup_beatmapset( response_model=BeatmapsetResp, description="获取单个谱面集详情。", ) +@asset_proxy_response async def get_beatmapset( db: Database, request: Request, @@ -174,9 +165,7 @@ async def get_beatmapset( # 先尝试从缓存获取 cached_resp = await cache_service.get_beatmapset_from_cache(beatmapset_id) if cached_resp: - # 处理资源代理 - processed_resp = await process_response_assets(cached_resp) - return processed_resp + return cached_resp try: beatmapset = await Beatmapset.get_or_fetch(db, fetcher, beatmapset_id) @@ -184,10 +173,7 @@ async def get_beatmapset( # 缓存结果 await cache_service.cache_beatmapset(resp) - - # 处理资源代理 - processed_resp = await process_response_assets(resp) - return processed_resp + return resp except HTTPError as exc: raise HTTPException(status_code=404, detail="Beatmapset not found") from exc diff --git a/app/router/v2/cache.py b/app/router/v2/cache.py index 0b1a396..a75b593 100644 --- a/app/router/v2/cache.py +++ b/app/router/v2/cache.py @@ -3,8 +3,6 @@ 提供缓存统计、清理和预热功能 """ -from __future__ import annotations - from app.dependencies.database import Redis from app.service.user_cache_service import get_user_cache_service diff --git a/app/router/v2/me.py b/app/router/v2/me.py index 5304e9d..ed89704 100644 --- a/app/router/v2/me.py +++ b/app/router/v2/me.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Annotated from app.database import MeResp, User diff --git a/app/router/v2/misc.py b/app/router/v2/misc.py index bd67695..5c8e18b 100644 --- a/app/router/v2/misc.py +++ b/app/router/v2/misc.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from datetime import UTC, datetime from app.config import settings diff --git a/app/router/v2/ranking.py b/app/router/v2/ranking.py index f2c6236..cde6c43 100644 --- a/app/router/v2/ranking.py +++ b/app/router/v2/ranking.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Annotated, Literal from app.config import settings diff --git a/app/router/v2/relationship.py b/app/router/v2/relationship.py index 4851e2a..89fd223 100644 --- a/app/router/v2/relationship.py +++ b/app/router/v2/relationship.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Annotated from app.database import Relationship, RelationshipResp, RelationshipType, User diff --git a/app/router/v2/room.py b/app/router/v2/room.py index 34fb6f7..66241e4 100644 --- a/app/router/v2/room.py +++ b/app/router/v2/room.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from datetime import UTC from typing import Annotated, Literal diff --git a/app/router/v2/router.py b/app/router/v2/router.py index 5ef8c3f..9b39315 100644 --- a/app/router/v2/router.py +++ b/app/router/v2/router.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from app.dependencies.rate_limit import LIMITERS from fastapi import APIRouter diff --git a/app/router/v2/score.py b/app/router/v2/score.py index 12be45d..a433da8 100644 --- a/app/router/v2/score.py +++ b/app/router/v2/score.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from datetime import UTC, date import time from typing import Annotated diff --git a/app/router/v2/session_verify.py b/app/router/v2/session_verify.py index add3d70..cf2808c 100644 --- a/app/router/v2/session_verify.py +++ b/app/router/v2/session_verify.py @@ -2,8 +2,6 @@ 会话验证路由 - 实现类似 osu! 的邮件验证流程 (API v2) """ -from __future__ import annotations - from typing import Annotated, Literal from app.auth import check_totp_backup_code, verify_totp_key_with_replay_protection diff --git a/app/router/v2/tags.py b/app/router/v2/tags.py index 810656d..d64d4aa 100644 --- a/app/router/v2/tags.py +++ b/app/router/v2/tags.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Annotated from app.database.beatmap import Beatmap diff --git a/app/router/v2/user.py b/app/router/v2/user.py index e4e41ea..4ffc976 100644 --- a/app/router/v2/user.py +++ b/app/router/v2/user.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from datetime import timedelta from typing import Annotated, Literal @@ -19,10 +17,10 @@ from app.database.user import SEARCH_INCLUDED from app.dependencies.api_version import APIVersion from app.dependencies.database import Database, get_redis from app.dependencies.user import get_current_user +from app.helpers.asset_proxy_helper import asset_proxy_response from app.log import log from app.models.score import GameMode from app.models.user import BeatmapsetType -from app.service.asset_proxy_helper import process_response_assets from app.service.user_cache_service import get_user_cache_service from app.utils import utcnow @@ -47,6 +45,7 @@ class BatchUserResponse(BaseModel): ) @router.get("/users/lookup", response_model=BatchUserResponse, include_in_schema=False) @router.get("/users/lookup/", response_model=BatchUserResponse, include_in_schema=False) +@asset_proxy_response async def get_users( session: Database, request: Request, @@ -89,28 +88,25 @@ async def get_users( # 异步缓存,不阻塞响应 background_task.add_task(cache_service.cache_user, user_resp) - # 处理资源代理 response = BatchUserResponse(users=cached_users) - processed_response = await process_response_assets(response) - return processed_response + return response else: searched_users = (await session.exec(select(User).limit(50))).all() users = [] for searched_user in searched_users: - if searched_user.id != BANCHOBOT_ID: - user_resp = await UserResp.from_db( - searched_user, - session, - include=SEARCH_INCLUDED, - ) - users.append(user_resp) - # 异步缓存 - background_task.add_task(cache_service.cache_user, user_resp) + if searched_user.id == BANCHOBOT_ID: + continue + user_resp = await UserResp.from_db( + searched_user, + session, + include=SEARCH_INCLUDED, + ) + users.append(user_resp) + # 异步缓存 + background_task.add_task(cache_service.cache_user, user_resp) - # 处理资源代理 response = BatchUserResponse(users=users) - processed_response = await process_response_assets(response) - return processed_response + return response @router.get( @@ -176,6 +172,7 @@ async def get_user_kudosu( description="通过用户 ID 或用户名获取单个用户的详细信息,并指定特定 ruleset。", tags=["用户"], ) +@asset_proxy_response async def get_user_info_ruleset( session: Database, background_task: BackgroundTasks, @@ -224,6 +221,7 @@ async def get_user_info_ruleset( description="通过用户 ID 或用户名获取单个用户的详细信息。", tags=["用户"], ) +@asset_proxy_response async def get_user_info( background_task: BackgroundTasks, session: Database, @@ -239,9 +237,7 @@ async def get_user_info( user_id_int = int(user_id) cached_user = await cache_service.get_user_from_cache(user_id_int) if cached_user: - # 处理资源代理 - processed_user = await process_response_assets(cached_user) - return processed_user + return cached_user searched_user = ( await session.exec( @@ -262,9 +258,7 @@ async def get_user_info( # 异步缓存结果 background_task.add_task(cache_service.cache_user, user_resp) - # 处理资源代理 - processed_user = await process_response_assets(user_resp) - return processed_user + return user_resp @router.get( @@ -274,6 +268,7 @@ async def get_user_info( description="获取指定用户特定类型的谱面集列表,如最常游玩、收藏等。", tags=["用户"], ) +@asset_proxy_response async def get_user_beatmapsets( session: Database, background_task: BackgroundTasks, @@ -354,6 +349,7 @@ async def get_user_beatmapsets( ), tags=["用户"], ) +@asset_proxy_response async def get_user_scores( request: Request, session: Database, @@ -381,8 +377,7 @@ async def get_user_scores( user_id, type, include_fails, mode, limit, offset, is_legacy_api ) if cached_scores is not None: - processed_scores = await process_response_assets(cached_scores) - return processed_scores + return cached_scores db_user = await session.get(User, user_id) if not db_user or db_user.id == BANCHOBOT_ID: @@ -437,6 +432,4 @@ async def get_user_scores( is_legacy_api, ) - # 处理资源代理 - processed_scores = await process_response_assets(score_responses) - return processed_scores + return score_responses diff --git a/app/service/asset_proxy_helper.py b/app/service/asset_proxy_helper.py deleted file mode 100644 index c41e77c..0000000 --- a/app/service/asset_proxy_helper.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -资源代理辅助函数和中间件 -""" - -from __future__ import annotations - -from typing import Any - -from app.config import settings -from app.service.asset_proxy_service import get_asset_proxy_service - -from fastapi import Request - - -async def process_response_assets(data: Any) -> Any: - """ - 根据配置处理响应数据中的资源URL - - Args: - data: API响应数据 - request: FastAPI请求对象 - - Returns: - 处理后的数据 - """ - if not settings.enable_asset_proxy: - return data - - asset_service = get_asset_proxy_service() - - # 仅URL替换模式 - return await asset_service.replace_asset_urls(data) - - -def should_process_asset_proxy(path: str) -> bool: - """ - 判断路径是否需要处理资源代理 - """ - # 只对特定的API端点处理资源代理 - asset_proxy_endpoints = [ - "/api/v1/users/", - "/api/v2/users/", - "/api/v1/me/", - "/api/v2/me/", - "/api/v2/beatmapsets/search", - "/api/v2/beatmapsets/lookup", - "/api/v2/beatmaps/", - "/api/v1/beatmaps/", - "/api/v2/beatmapsets/", - # 可以根据需要添加更多端点 - ] - - return any(path.startswith(endpoint) for endpoint in asset_proxy_endpoints) - - -# 响应处理装饰器 -def asset_proxy_response(func): - """ - 装饰器:自动处理响应中的资源URL - """ - - async def wrapper(*args, **kwargs): - # 获取request对象 - request = None - for arg in args: - if isinstance(arg, Request): - request = arg - break - - # 执行原函数 - result = await func(*args, **kwargs) - - # 如果有request对象且启用了资源代理,则处理响应 - if request and settings.enable_asset_proxy and should_process_asset_proxy(request.url.path): - result = await process_response_assets(result) - - return result - - return wrapper diff --git a/app/service/asset_proxy_service.py b/app/service/asset_proxy_service.py deleted file mode 100644 index 72fa54a..0000000 --- a/app/service/asset_proxy_service.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -资源文件代理服务 -提供URL替换方案:将osu!官方资源URL替换为自定义域名 -""" - -from __future__ import annotations - -import re -from typing import Any - -from app.config import settings - - -class AssetProxyService: - """资源代理服务 - 仅URL替换模式""" - - def __init__(self): - # 从配置获取自定义assets域名和前缀 - self.custom_asset_domain = settings.custom_asset_domain - self.asset_proxy_prefix = settings.asset_proxy_prefix - self.avatar_proxy_prefix = settings.avatar_proxy_prefix - self.beatmap_proxy_prefix = settings.beatmap_proxy_prefix - # 音频代理接口URL - self.audio_proxy_base_url = f"{settings.server_url}api/private/audio/beatmapset" - - async def replace_asset_urls(self, data: Any) -> Any: - """ - 递归替换数据中的osu!资源URL为自定义域名 - """ - # 处理Pydantic模型 - if hasattr(data, "model_dump"): - # 转换为字典,处理后再转换回模型 - data_dict = data.model_dump() - processed_dict = await self.replace_asset_urls(data_dict) - # 尝试从字典重新创建模型 - try: - return data.__class__(**processed_dict) - except Exception: - # 如果重新创建失败,返回字典 - return processed_dict - elif isinstance(data, dict): - result = {} - for key, value in data.items(): - result[key] = await self.replace_asset_urls(value) - return result - elif isinstance(data, list): - return [await self.replace_asset_urls(item) for item in data] - elif isinstance(data, str): - # 替换各种osu!资源域名 - result = data - - # 替换 assets.ppy.sh (用户头像、封面、奖章等) - result = re.sub( - r"https://assets\.ppy\.sh/", f"https://{self.asset_proxy_prefix}.{self.custom_asset_domain}/", result - ) - - # 替换 b.ppy.sh 预览音频为我们的音频代理接口 - # 匹配 https://b.ppy.sh/preview/{beatmapset_id}.mp3 格式 - result = re.sub(r"https://b\.ppy\.sh/preview/(\d+)\.mp3", rf"{self.audio_proxy_base_url}/\1", result) - - # 匹配 //b.ppy.sh/preview/{beatmapset_id}.mp3 格式 - result = re.sub(r"//b\.ppy\.sh/preview/(\d+)\.mp3", rf"{self.audio_proxy_base_url}/\1", result) - - # 替换 a.ppy.sh 头像 - result = re.sub( - r"https://a\.ppy\.sh/", f"https://{self.avatar_proxy_prefix}.{self.custom_asset_domain}/", result - ) - - return result - else: - return data - - -# 全局实例 -_asset_proxy_service: AssetProxyService | None = None - - -def get_asset_proxy_service() -> AssetProxyService: - """获取资源代理服务实例""" - global _asset_proxy_service - if _asset_proxy_service is None: - _asset_proxy_service = AssetProxyService() - return _asset_proxy_service diff --git a/app/service/ranking_cache_service.py b/app/service/ranking_cache_service.py index 0d9dd37..149fdab 100644 --- a/app/service/ranking_cache_service.py +++ b/app/service/ranking_cache_service.py @@ -12,9 +12,9 @@ from typing import TYPE_CHECKING, Literal from app.config import settings from app.database.statistics import UserStatistics, UserStatisticsResp +from app.helpers.asset_proxy_helper import replace_asset_urls from app.log import logger from app.models.score import GameMode -from app.service.asset_proxy_service import get_asset_proxy_service from app.utils import utcnow from redis.asyncio import Redis @@ -357,16 +357,15 @@ class RankingCacheService: for statistics in statistics_data: user_stats_resp = await UserStatisticsResp.from_db(statistics, session, None, include) + user_dict = user_stats_resp.model_dump() + # 应用资源代理处理 if settings.enable_asset_proxy: try: - asset_proxy_service = get_asset_proxy_service() - user_stats_resp = await asset_proxy_service.replace_asset_urls(user_stats_resp) + user_dict = await replace_asset_urls(user_dict) except Exception as e: logger.warning(f"Asset proxy processing failed for ranking cache: {e}") - # 将 UserStatisticsResp 转换为字典,处理所有序列化问题 - user_dict = json.loads(user_stats_resp.model_dump_json()) ranking_data.append(user_dict) # 缓存这一页的数据 diff --git a/app/service/user_cache_service.py b/app/service/user_cache_service.py index 4a40229..4aef7e6 100644 --- a/app/service/user_cache_service.py +++ b/app/service/user_cache_service.py @@ -15,9 +15,9 @@ from app.database import User, UserResp from app.database.score import LegacyScoreResp, ScoreResp from app.database.user import SEARCH_INCLUDED from app.dependencies.database import with_db +from app.helpers.asset_proxy_helper import replace_asset_urls from app.log import logger from app.models.score import GameMode -from app.service.asset_proxy_service import get_asset_proxy_service from redis.asyncio import Redis from sqlmodel import col, select @@ -318,8 +318,7 @@ class UserCacheService: # 应用资源代理处理 if settings.enable_asset_proxy: try: - asset_proxy_service = get_asset_proxy_service() - user_resp = await asset_proxy_service.replace_asset_urls(user_resp) + user_resp = await replace_asset_urls(user_resp) except Exception as e: logger.warning(f"Asset proxy processing failed for user cache {user.id}: {e}") diff --git a/pyproject.toml b/pyproject.toml index 2f99cda..4170f81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ ignore = [ "migrations/**/*.py" = ["INP001"] ".github/**/*.py" = ["INP001"] "app/achievements/*.py" = ["INP001", "ARG"] -"app/router/**/*.py" = ["ARG001"] +"app/router/**/*.py" = ["ARG001", "I002"] [tool.ruff.lint.isort] force-sort-within-sections = true