from __future__ import annotations from datetime import datetime from typing import Literal from app.database.beatmap import Beatmap, calculate_beatmap_attributes from app.database.beatmap_playcounts import BeatmapPlaycounts from app.database.beatmapset import Beatmapset from app.database.favourite_beatmapset import FavouriteBeatmapset from app.database.score import Score from app.dependencies.database import get_db, get_redis from app.dependencies.fetcher import get_fetcher from app.fetcher import Fetcher from app.models.beatmap import BeatmapRankStatus, Genre, Language from app.models.mods import int_to_mods from app.models.score import MODE_TO_INT, GameMode from .router import AllStrModel, router from fastapi import Depends, Query from redis.asyncio import Redis from sqlmodel import col, func, select from sqlmodel.ext.asyncio.session import AsyncSession class V1Beatmap(AllStrModel): approved: BeatmapRankStatus submit_date: datetime approved_date: datetime | None = None last_update: datetime artist: str artist_unicode: str beatmap_id: int beatmapset_id: int bpm: float creator: str creator_id: int difficultyrating: float diff_aim: float | None = None diff_speed: float | None = None diff_size: float # CS diff_overall: float # OD diff_approach: float # AR diff_drain: float # HP hit_length: int source: str genre_id: Genre language_id: Language title: str title_unicode: str total_length: int version: str file_md5: str mode: int tags: str favourite_count: int rating: float playcount: int passcount: int count_normal: int count_slider: int count_spinner: int max_combo: int | None = None storyboard: bool video: bool download_unavailable: bool audio_unavailable: bool @classmethod async def from_db( cls, session: AsyncSession, db_beatmap: Beatmap, diff_aim: float | None = None, diff_speed: float | None = None, ) -> "V1Beatmap": return cls( approved=db_beatmap.beatmap_status, submit_date=db_beatmap.beatmapset.submitted_date, approved_date=db_beatmap.beatmapset.ranked_date, last_update=db_beatmap.last_updated, artist=db_beatmap.beatmapset.artist, beatmap_id=db_beatmap.id, beatmapset_id=db_beatmap.beatmapset.id, bpm=db_beatmap.bpm, creator=db_beatmap.beatmapset.creator, creator_id=db_beatmap.beatmapset.user_id, difficultyrating=db_beatmap.difficulty_rating, diff_aim=diff_aim, diff_speed=diff_speed, diff_size=db_beatmap.cs, diff_overall=db_beatmap.accuracy, diff_approach=db_beatmap.ar, diff_drain=db_beatmap.drain, hit_length=db_beatmap.hit_length, source=db_beatmap.beatmapset.source, genre_id=db_beatmap.beatmapset.beatmap_genre, language_id=db_beatmap.beatmapset.beatmap_language, title=db_beatmap.beatmapset.title, total_length=db_beatmap.total_length, version=db_beatmap.version, file_md5=db_beatmap.checksum, mode=MODE_TO_INT[db_beatmap.mode], tags=db_beatmap.beatmapset.tags, favourite_count=( await session.exec( select(func.count()) .select_from(FavouriteBeatmapset) .where( FavouriteBeatmapset.beatmapset_id == db_beatmap.beatmapset.id ) ) ).one(), rating=0, # TODO playcount=( await session.exec( select(func.count()) .select_from(BeatmapPlaycounts) .where(BeatmapPlaycounts.beatmap_id == db_beatmap.id) ) ).one(), passcount=( await session.exec( select(func.count()) .select_from(Score) .where( Score.beatmap_id == db_beatmap.id, col(Score.passed).is_(True), ) ) ).one(), count_normal=db_beatmap.count_circles, count_slider=db_beatmap.count_sliders, count_spinner=db_beatmap.count_spinners, max_combo=db_beatmap.max_combo, storyboard=db_beatmap.beatmapset.storyboard, video=db_beatmap.beatmapset.video, download_unavailable=db_beatmap.beatmapset.download_disabled, audio_unavailable=db_beatmap.beatmapset.download_disabled, artist_unicode=db_beatmap.beatmapset.artist_unicode, title_unicode=db_beatmap.beatmapset.title_unicode, ) @router.get( "/get_beatmaps", name="获取谱面", response_model=list[V1Beatmap], description="根据指定条件搜索谱面。", ) async def get_beatmaps( since: datetime | None = Query(None, description="自指定时间后拥有排行榜的谱面"), beatmapset_id: int | None = Query(None, alias="s", description="谱面集 ID"), beatmap_id: int | None = Query(None, alias="b", description="谱面 ID"), user: str | None = Query(None, alias="u", description="谱师"), type: Literal["string", "id"] | None = Query( None, description="用户类型:string 用户名称 / id 用户 ID" ), ruleset_id: int | None = Query( None, alias="m", description="Ruleset ID", ge=0, le=3 ), # TODO convert: bool = Query(False, alias="a", description="转谱"), # TODO checksum: str | None = Query(None, alias="h", description="谱面文件 MD5"), limit: int = Query(500, ge=1, le=500, description="返回结果数量限制"), mods: int = Query(0, description="应用到谱面属性的 MOD"), session: AsyncSession = Depends(get_db), redis: Redis = Depends(get_redis), fetcher: Fetcher = Depends(get_fetcher), ): beatmaps: list[Beatmap] = [] results = [] if beatmap_id is not None: beatmaps.append(await Beatmap.get_or_fetch(session, fetcher, beatmap_id)) elif checksum is not None: beatmaps.append(await Beatmap.get_or_fetch(session, fetcher, md5=checksum)) elif beatmapset_id is not None: beatmapset = await Beatmapset.get_or_fetch(session, fetcher, beatmapset_id) await beatmapset.awaitable_attrs.beatmaps if len(beatmapset.beatmaps) > limit: beatmaps = beatmapset.beatmaps[:limit] else: beatmaps = beatmapset.beatmaps elif user is not None: where = ( Beatmapset.user_id == user if type == "id" or user.isdigit() else Beatmapset.creator == user ) beatmapsets = (await session.exec(select(Beatmapset).where(where))).all() for beatmapset in beatmapsets: if len(beatmaps) >= limit: break beatmaps.extend(beatmapset.beatmaps) elif since is not None: beatmapsets = ( await session.exec( select(Beatmapset) .where(col(Beatmapset.ranked_date) > since) .limit(limit) ) ).all() for beatmapset in beatmapsets: if len(beatmaps) >= limit: break beatmaps.extend(beatmapset.beatmaps) for beatmap in beatmaps: if beatmap.mode == GameMode.OSU: try: attrs = await calculate_beatmap_attributes( beatmap.id, beatmap.mode, sorted(int_to_mods(mods), key=lambda m: m["acronym"]), redis, fetcher, ) results.append( await V1Beatmap.from_db( session, beatmap, attrs.aim_difficulty, attrs.speed_difficulty ) ) continue except Exception: ... results.append(await V1Beatmap.from_db(session, beatmap, None, None)) return results