from __future__ import annotations import re from typing import Annotated, Literal from urllib.parse import parse_qs from app.database import Beatmap, Beatmapset, BeatmapsetResp, FavouriteBeatmapset, User from app.database.beatmapset import SearchBeatmapsetsResp from app.dependencies.database import engine, get_db from app.dependencies.fetcher import get_fetcher from app.dependencies.user import get_client_user, get_current_user from app.fetcher import Fetcher from app.models.beatmap import SearchQueryModel from .router import router from fastapi import ( BackgroundTasks, Depends, Form, HTTPException, Path, Query, Request, Security, ) from fastapi.responses import RedirectResponse from httpx import HTTPError from sqlmodel import exists, select from sqlmodel.ext.asyncio.session import AsyncSession async def _save_to_db(sets: SearchBeatmapsetsResp): async with AsyncSession(engine) as session: for s in sets.beatmapsets: if not ( await session.exec(select(exists()).where(Beatmapset.id == s.id)) ).first(): await Beatmapset.from_resp(session, s) @router.get( "/beatmapsets/search", name="搜索谱面集", tags=["谱面集"], response_model=SearchBeatmapsetsResp, ) async def search_beatmapset( query: Annotated[SearchQueryModel, Query(...)], request: Request, background_tasks: BackgroundTasks, current_user: User = Security(get_current_user, scopes=["public"]), db: AsyncSession = Depends(get_db), fetcher: Fetcher = Depends(get_fetcher), ): params = parse_qs(qs=request.url.query, keep_blank_values=True) cursor = {} for k, v in params.items(): match = re.match(r"cursor\[(\w+)\]", k) if match: cursor[match.group(1)] = v[0] if v else None if ( "recommended" in query.c or len(query.r) > 0 or query.played or "follows" in query.c or "mine" in query.s or "favourites" in query.s ): # TODO: search locally return SearchBeatmapsetsResp(total=0, beatmapsets=[]) try: sets = await fetcher.search_beatmapset(query, cursor) background_tasks.add_task(_save_to_db, sets) except HTTPError as e: raise HTTPException(status_code=500, detail=str(e)) return sets @router.get( "/beatmapsets/lookup", tags=["谱面集"], name="查询谱面集 (通过谱面 ID)", response_model=BeatmapsetResp, description=("通过谱面 ID 查询所属谱面集。"), ) async def lookup_beatmapset( beatmap_id: int = Query(description="谱面 ID"), current_user: User = Security(get_current_user, scopes=["public"]), db: AsyncSession = Depends(get_db), fetcher: Fetcher = Depends(get_fetcher), ): beatmap = await Beatmap.get_or_fetch(db, fetcher, bid=beatmap_id) resp = await BeatmapsetResp.from_db( beatmap.beatmapset, session=db, user=current_user ) return resp @router.get( "/beatmapsets/{beatmapset_id}", tags=["谱面集"], name="获取谱面集详情", response_model=BeatmapsetResp, description="获取单个谱面集详情。", ) async def get_beatmapset( beatmapset_id: int = Path(..., description="谱面集 ID"), current_user: User = Security(get_current_user, scopes=["public"]), db: AsyncSession = Depends(get_db), fetcher: Fetcher = Depends(get_fetcher), ): try: beatmapset = await Beatmapset.get_or_fetch(db, fetcher, beatmapset_id) return await BeatmapsetResp.from_db( beatmapset, session=db, include=["recent_favourites"], user=current_user ) except HTTPError: raise HTTPException(status_code=404, detail="Beatmapset not found") @router.get( "/beatmapsets/{beatmapset_id}/download", tags=["谱面集"], name="下载谱面集", description="**客户端专属**\n下载谱面集文件。若用户国家为 CN 则跳转国内镜像。", ) async def download_beatmapset( beatmapset_id: int = Path(..., description="谱面集 ID"), no_video: bool = Query(True, alias="noVideo", description="是否下载无视频版本"), current_user: User = Security(get_client_user), ): if current_user.country_code == "CN": return RedirectResponse( f"https://txy1.sayobot.cn/beatmaps/download/" f"{'novideo' if no_video else 'full'}/{beatmapset_id}?server=auto" ) else: return RedirectResponse( f"https://api.nerinyan.moe/d/{beatmapset_id}?noVideo={no_video}" ) @router.post( "/beatmapsets/{beatmapset_id}/favourites", tags=["谱面集"], name="收藏或取消收藏谱面集", description="**客户端专属**\n收藏或取消收藏指定谱面集。", ) async def favourite_beatmapset( beatmapset_id: int = Path(..., description="谱面集 ID"), action: Literal["favourite", "unfavourite"] = Form( description="操作类型:favourite 收藏 / unfavourite 取消收藏" ), current_user: User = Security(get_client_user), db: AsyncSession = Depends(get_db), ): assert current_user.id is not None existing_favourite = ( await db.exec( select(FavouriteBeatmapset).where( FavouriteBeatmapset.user_id == current_user.id, FavouriteBeatmapset.beatmapset_id == beatmapset_id, ) ) ).first() if (action == "favourite" and existing_favourite) or ( action == "unfavourite" and not existing_favourite ): return if action == "favourite": favourite = FavouriteBeatmapset( user_id=current_user.id, beatmapset_id=beatmapset_id ) db.add(favourite) else: await db.delete(existing_favourite) await db.commit()