Technical Details: https://blog.mxgame.top/2025/11/22/An-On-Demand-Design-Within-SQLModel/
244 lines
8.0 KiB
Python
244 lines
8.0 KiB
Python
from datetime import datetime
|
|
from typing import ClassVar, NotRequired, TypedDict
|
|
|
|
from app.models.room import (
|
|
MatchType,
|
|
QueueMode,
|
|
RoomCategory,
|
|
RoomDifficultyRange,
|
|
RoomPlaylistItemStats,
|
|
RoomStatus,
|
|
)
|
|
from app.utils import utcnow
|
|
|
|
from ._base import DatabaseModel, included, ondemand
|
|
from .item_attempts_count import ItemAttemptsCount, ItemAttemptsCountDict, ItemAttemptsCountModel
|
|
from .playlists import Playlist, PlaylistDict, PlaylistModel
|
|
from .room_participated_user import RoomParticipatedUser
|
|
from .user import User, UserDict, UserModel
|
|
|
|
from pydantic import field_validator
|
|
from sqlalchemy.ext.asyncio import AsyncAttrs
|
|
from sqlmodel import BigInteger, Column, DateTime, Field, ForeignKey, Relationship, SQLModel, col, select
|
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
|
|
|
|
class RoomDict(TypedDict):
|
|
id: int
|
|
name: str
|
|
category: RoomCategory
|
|
status: RoomStatus
|
|
type: MatchType
|
|
duration: int | None
|
|
starts_at: datetime | None
|
|
ends_at: datetime | None
|
|
max_attempts: int | None
|
|
participant_count: int
|
|
channel_id: int
|
|
queue_mode: QueueMode
|
|
auto_skip: bool
|
|
auto_start_duration: int
|
|
has_password: NotRequired[bool]
|
|
current_playlist_item: NotRequired["PlaylistDict | None"]
|
|
playlist: NotRequired[list["PlaylistDict"]]
|
|
playlist_item_stats: NotRequired[RoomPlaylistItemStats]
|
|
difficulty_range: NotRequired[RoomDifficultyRange]
|
|
host: NotRequired[UserDict]
|
|
recent_participants: NotRequired[list[UserDict]]
|
|
current_user_score: NotRequired["ItemAttemptsCountDict | None"]
|
|
|
|
|
|
class RoomModel(DatabaseModel[RoomDict]):
|
|
SHOW_RESPONSE_INCLUDES: ClassVar[list[str]] = [
|
|
"current_user_score.playlist_item_attempts",
|
|
"host.country",
|
|
"playlist.beatmap.beatmapset",
|
|
"playlist.beatmap.checksum",
|
|
"playlist.beatmap.max_combo",
|
|
"recent_participants",
|
|
]
|
|
|
|
id: int = Field(default=None, primary_key=True, index=True)
|
|
name: str = Field(index=True)
|
|
category: RoomCategory = Field(default=RoomCategory.NORMAL, index=True)
|
|
status: RoomStatus
|
|
type: MatchType
|
|
duration: int | None = Field(default=None) # minutes
|
|
starts_at: datetime | None = Field(
|
|
sa_column=Column(
|
|
DateTime(timezone=True),
|
|
),
|
|
default_factory=utcnow,
|
|
)
|
|
ends_at: datetime | None = Field(
|
|
sa_column=Column(
|
|
DateTime(timezone=True),
|
|
),
|
|
default=None,
|
|
)
|
|
max_attempts: int | None = Field(default=None) # playlists
|
|
participant_count: int = Field(default=0)
|
|
channel_id: int = 0
|
|
queue_mode: QueueMode
|
|
auto_skip: bool
|
|
|
|
auto_start_duration: int
|
|
|
|
@field_validator("channel_id", mode="before")
|
|
@classmethod
|
|
def validate_channel_id(cls, v):
|
|
"""将 None 转换为 0"""
|
|
if v is None:
|
|
return 0
|
|
return v
|
|
|
|
@included
|
|
@staticmethod
|
|
async def has_password(_session: AsyncSession, room: "Room") -> bool:
|
|
return bool(room.password)
|
|
|
|
@ondemand
|
|
@staticmethod
|
|
async def current_playlist_item(
|
|
_session: AsyncSession, room: "Room", includes: list[str] | None = None
|
|
) -> "PlaylistDict | None":
|
|
playlists = await room.awaitable_attrs.playlist
|
|
if not playlists:
|
|
return None
|
|
return await PlaylistModel.transform(playlists[-1], includes=includes)
|
|
|
|
@ondemand
|
|
@staticmethod
|
|
async def playlist(_session: AsyncSession, room: "Room", includes: list[str] | None = None) -> list["PlaylistDict"]:
|
|
playlists = await room.awaitable_attrs.playlist
|
|
result: list[PlaylistDict] = []
|
|
for playlist_item in playlists:
|
|
result.append(await PlaylistModel.transform(playlist_item, includes=includes))
|
|
return result
|
|
|
|
@ondemand
|
|
@staticmethod
|
|
async def playlist_item_stats(_session: AsyncSession, room: "Room") -> RoomPlaylistItemStats:
|
|
playlists = await room.awaitable_attrs.playlist
|
|
stats = RoomPlaylistItemStats(count_active=0, count_total=0, ruleset_ids=[])
|
|
rulesets: set[int] = set()
|
|
for playlist in playlists:
|
|
stats.count_total += 1
|
|
if not playlist.expired:
|
|
stats.count_active += 1
|
|
rulesets.add(playlist.ruleset_id)
|
|
stats.ruleset_ids = list(rulesets)
|
|
return stats
|
|
|
|
@ondemand
|
|
@staticmethod
|
|
async def difficulty_range(_session: AsyncSession, room: "Room") -> RoomDifficultyRange:
|
|
playlists = await room.awaitable_attrs.playlist
|
|
if not playlists:
|
|
return RoomDifficultyRange(min=0.0, max=0.0)
|
|
min_diff = float("inf")
|
|
max_diff = float("-inf")
|
|
for playlist in playlists:
|
|
rating = playlist.beatmap.difficulty_rating
|
|
min_diff = min(min_diff, rating)
|
|
max_diff = max(max_diff, rating)
|
|
if min_diff == float("inf"):
|
|
min_diff = 0.0
|
|
if max_diff == float("-inf"):
|
|
max_diff = 0.0
|
|
return RoomDifficultyRange(min=min_diff, max=max_diff)
|
|
|
|
@ondemand
|
|
@staticmethod
|
|
async def host(_session: AsyncSession, room: "Room", includes: list[str] | None = None) -> UserDict:
|
|
host_user = await room.awaitable_attrs.host
|
|
return await UserModel.transform(host_user, includes=includes)
|
|
|
|
@ondemand
|
|
@staticmethod
|
|
async def recent_participants(session: AsyncSession, room: "Room") -> list[UserDict]:
|
|
participants: list[UserDict] = []
|
|
if room.category == RoomCategory.REALTIME:
|
|
query = (
|
|
select(RoomParticipatedUser)
|
|
.where(
|
|
RoomParticipatedUser.room_id == room.id,
|
|
col(RoomParticipatedUser.left_at).is_(None),
|
|
)
|
|
.limit(8)
|
|
.order_by(col(RoomParticipatedUser.joined_at).desc())
|
|
)
|
|
else:
|
|
query = (
|
|
select(RoomParticipatedUser)
|
|
.where(
|
|
RoomParticipatedUser.room_id == room.id,
|
|
)
|
|
.limit(8)
|
|
.order_by(col(RoomParticipatedUser.joined_at).desc())
|
|
)
|
|
for recent_participant in await session.exec(query):
|
|
user_instance = await recent_participant.awaitable_attrs.user
|
|
participants.append(await UserModel.transform(user_instance))
|
|
return participants
|
|
|
|
@ondemand
|
|
@staticmethod
|
|
async def current_user_score(
|
|
session: AsyncSession, room: "Room", includes: list[str] | None = None
|
|
) -> "ItemAttemptsCountDict | None":
|
|
item_attempt = (
|
|
await session.exec(
|
|
select(ItemAttemptsCount).where(
|
|
ItemAttemptsCount.room_id == room.id,
|
|
)
|
|
)
|
|
).first()
|
|
if item_attempt is None:
|
|
return None
|
|
|
|
return await ItemAttemptsCountModel.transform(item_attempt, includes=includes)
|
|
|
|
|
|
class Room(AsyncAttrs, RoomModel, table=True):
|
|
__tablename__: str = "rooms"
|
|
|
|
host_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True))
|
|
password: str | None = Field(default=None)
|
|
|
|
host: User = Relationship()
|
|
playlist: list[Playlist] = Relationship(
|
|
sa_relationship_kwargs={
|
|
"lazy": "selectin",
|
|
"cascade": "all, delete-orphan",
|
|
"overlaps": "room",
|
|
}
|
|
)
|
|
|
|
|
|
class APIUploadedRoom(SQLModel):
|
|
name: str = Field(index=True)
|
|
category: RoomCategory = Field(default=RoomCategory.NORMAL, index=True)
|
|
status: RoomStatus
|
|
type: MatchType
|
|
duration: int | None = Field(default=None) # minutes
|
|
starts_at: datetime | None = Field(
|
|
sa_column=Column(
|
|
DateTime(timezone=True),
|
|
),
|
|
default_factory=utcnow,
|
|
)
|
|
ends_at: datetime | None = Field(
|
|
sa_column=Column(
|
|
DateTime(timezone=True),
|
|
),
|
|
default=None,
|
|
)
|
|
max_attempts: int | None = Field(default=None) # playlists
|
|
participant_count: int = Field(default=0)
|
|
channel_id: int = 0
|
|
queue_mode: QueueMode
|
|
auto_skip: bool
|
|
auto_start_duration: int
|
|
playlist: list[Playlist] = Field(default_factory=list)
|