maimai_py.models

  1from dataclasses import dataclass
  2from datetime import datetime
  3from typing import Any, MutableMapping, Optional, Sequence, Union
  4
  5from maimai_py.enums import *
  6from maimai_py.exceptions import *
  7from maimai_py.utils import UNSET, _UnsetSentinel
  8
  9
 10@dataclass
 11class Song:
 12    __slots__ = ("id", "title", "artist", "genre", "bpm", "map", "version", "rights", "aliases", "disabled", "difficulties")
 13
 14    id: int
 15    title: str
 16    artist: str
 17    genre: Genre
 18    bpm: int
 19    map: Optional[str]
 20    version: int
 21    rights: Optional[str]
 22    aliases: Optional[list[str]]
 23    disabled: bool
 24    difficulties: "SongDifficulties"
 25
 26    def get_difficulty(self, type: SongType, level_index: LevelIndex) -> Optional["SongDifficulty"]:
 27        """Get the exact difficulty of this song by type and level index.
 28
 29        Args:
 30            type: The type of the song (DX, STANDARD, UTAGE).
 31            level_index: The level index of the difficulty.
 32        Returns:
 33            The difficulty object if found, otherwise None.
 34        """
 35        if type == SongType.DX:
 36            return next((diff for diff in self.difficulties.dx if diff.level_index == level_index), None)
 37        if type == SongType.STANDARD:
 38            return next((diff for diff in self.difficulties.standard if diff.level_index == level_index), None)
 39        if type == SongType.UTAGE:
 40            return next(iter(self.difficulties.utage), None)
 41
 42    def get_difficulties(self, song_type: Union[SongType, _UnsetSentinel] = UNSET) -> Sequence["SongDifficulty"]:
 43        """Get all difficulties of the song, optionally filtered by type.
 44
 45        Args:
 46            song_type: The type of the song (DX, STANDARD, UTAGE). If UNSET, returns all types.
 47        Returns:
 48            A sequence of difficulties matching the specified type.
 49        """
 50        if isinstance(song_type, _UnsetSentinel):
 51            return self.difficulties.standard + self.difficulties.dx + self.difficulties.utage
 52        if song_type == SongType.DX:
 53            return self.difficulties.dx
 54        if song_type == SongType.STANDARD:
 55            return self.difficulties.standard
 56        if song_type == SongType.UTAGE:
 57            return self.difficulties.utage
 58
 59    def get_divingfish_id(self, type: SongType, level_index: LevelIndex) -> int:
 60        """Get the Diving Fish ID for a specific difficulty of this song.
 61
 62        Args:
 63            type: The type of the song (DX, STANDARD, UTAGE).
 64            level_index: The level index of the difficulty.
 65        Returns:
 66            The Diving Fish ID for the specified difficulty.
 67        """
 68        difficulty = self.get_difficulty(type, level_index)
 69        if difficulty is None:
 70            raise ValueError(f"No difficulty found for type {type} and level index {level_index}")
 71        if difficulty.type == SongType.DX:
 72            return self.id + 10000
 73        if difficulty.type == SongType.UTAGE:
 74            return self.id + 100000
 75        return self.id
 76
 77    def get_divingfish_ids(self, song_type: Union[SongType, _UnsetSentinel] = UNSET) -> set[int]:
 78        """Get a set of Diving Fish IDs for all difficulties of this song.
 79
 80        Args:
 81            song_type: The type of the song (DX, STANDARD, UTAGE). If UNSET, returns IDs for all types.
 82        Returns:
 83            A set of Diving Fish IDs for the specified type or all types if UNSET.
 84        """
 85        ids = set()
 86        for difficulty in self.get_difficulties(song_type):
 87            ids.add(self.get_divingfish_id(difficulty.type, difficulty.level_index))
 88        return ids
 89
 90
 91@dataclass
 92class SongDifficulties:
 93    __slots__ = ("standard", "dx", "utage")
 94
 95    standard: list["SongDifficulty"]
 96    dx: list["SongDifficulty"]
 97    utage: list["SongDifficultyUtage"]
 98
 99
100@dataclass
101class CurveObject:
102    __slots__ = ("sample_size", "fit_level_value", "avg_achievements", "stdev_achievements", "avg_dx_score", "rate_sample_size", "fc_sample_size")
103
104    sample_size: int
105    fit_level_value: float
106    avg_achievements: float
107    stdev_achievements: float
108    avg_dx_score: float
109    rate_sample_size: dict[RateType, int]
110    fc_sample_size: dict[FCType, int]
111
112
113@dataclass
114class SongDifficulty:
115    __slots__ = (
116        "id",
117        "type",
118        "level",
119        "level_value",
120        "level_index",
121        "note_designer",
122        "version",
123        "tap_num",
124        "hold_num",
125        "slide_num",
126        "touch_num",
127        "break_num",
128        "curve",
129    )
130
131    type: SongType
132    level: str
133    level_value: float
134    level_index: LevelIndex
135    note_designer: str
136    version: int
137    tap_num: int
138    hold_num: int
139    slide_num: int
140    touch_num: int
141    break_num: int
142    curve: Optional[CurveObject]
143
144
145@dataclass
146class SongDifficultyUtage(SongDifficulty):
147    __slots__ = ("kanji", "description", "is_buddy")
148
149    kanji: str
150    description: str
151    is_buddy: bool
152
153
154@dataclass
155class SongAlias:
156    __slots__ = ("song_id", "aliases")
157
158    song_id: int
159    aliases: list[str]
160
161
162@dataclass
163class PlayerIdentifier:
164    qq: Optional[int] = None
165    username: Optional[str] = None
166    friend_code: Optional[int] = None
167    credentials: Union[str, MutableMapping[str, Any], None] = None
168
169    def __post_init__(self):
170        if self.qq is None and self.username is None and self.friend_code is None and self.credentials is None:
171            raise InvalidPlayerIdentifierError("At least one of the following must be provided: qq, username, friend_code, credentials")
172
173    def _as_diving_fish(self) -> dict[str, Any]:
174        if self.qq:
175            return {"qq": str(self.qq)}
176        elif self.username:
177            return {"username": self.username}
178        elif self.friend_code:
179            raise InvalidPlayerIdentifierError("Friend code is not applicable for Diving Fish")
180        else:
181            raise InvalidPlayerIdentifierError("No valid identifier provided")
182
183    def _as_lxns(self) -> str:
184        if self.friend_code:
185            return str(self.friend_code)
186        elif self.qq:
187            return f"qq/{str(self.qq)}"
188        elif self.username:
189            raise InvalidPlayerIdentifierError("Username is not applicable for LXNS")
190        else:
191            raise InvalidPlayerIdentifierError("No valid identifier provided")
192
193
194@dataclass
195class PlayerItem:
196    @staticmethod
197    def _namespace() -> str:
198        raise NotImplementedError
199
200
201@dataclass
202class PlayerTrophy(PlayerItem):
203    __slots__ = ("id", "name", "color")
204
205    id: int
206    name: str
207    color: str
208
209    @staticmethod
210    def _namespace():
211        return "trophies"
212
213
214@dataclass
215class PlayerIcon(PlayerItem):
216    __slots__ = ("id", "name", "description", "genre")
217
218    id: int
219    name: str
220    description: Optional[str]
221    genre: Optional[str]
222
223    @staticmethod
224    def _namespace():
225        return "icons"
226
227
228@dataclass
229class PlayerNamePlate(PlayerItem):
230    __slots__ = ("id", "name", "description", "genre")
231
232    id: int
233    name: str
234    description: Optional[str]
235    genre: Optional[str]
236
237    @staticmethod
238    def _namespace():
239        return "nameplates"
240
241
242@dataclass
243class PlayerFrame(PlayerItem):
244    __slots__ = ("id", "name", "description", "genre")
245
246    id: int
247    name: str
248    description: Optional[str]
249    genre: Optional[str]
250
251    @staticmethod
252    def _namespace():
253        return "frames"
254
255
256@dataclass
257class PlayerPartner(PlayerItem):
258    __slots__ = ("id", "name")
259
260    id: int
261    name: str
262
263    @staticmethod
264    def _namespace():
265        return "partners"
266
267
268@dataclass
269class PlayerChara(PlayerItem):
270    __slots__ = ("id", "name")
271
272    id: int
273    name: str
274
275    @staticmethod
276    def _namespace():
277        return "charas"
278
279
280@dataclass
281class PlayerRegion:
282    __slots__ = ("region_id", "region_name", "play_count", "created_at")
283
284    region_id: int
285    region_name: str
286    play_count: int
287    created_at: datetime
288
289
290@dataclass
291class Player:
292    __slots__ = ("identifier", "name", "rating")
293
294    name: str
295    rating: int
296
297
298@dataclass
299class DivingFishPlayer(Player):
300    __slots__ = ("nickname", "plate", "additional_rating")
301
302    nickname: str
303    plate: str
304    additional_rating: int
305
306
307@dataclass
308class LXNSPlayer(Player):
309    __slots__ = ("friend_code", "course_rank", "class_rank", "star", "frame", "icon", "trophy", "name_plate", "upload_time")
310
311    friend_code: int
312    course_rank: int
313    class_rank: int
314    star: int
315    frame: Optional[PlayerFrame]
316    icon: Optional[PlayerIcon]
317    trophy: Optional[PlayerTrophy]
318    name_plate: Optional[PlayerNamePlate]
319    upload_time: str
320
321
322@dataclass
323class ArcadePlayer(Player):
324    __slots__ = ("is_login", "icon", "trophy", "name_plate")
325
326    is_login: bool
327    icon: Optional[PlayerIcon]
328    trophy: Optional[PlayerTrophy]
329    name_plate: Optional[PlayerNamePlate]
330
331
332@dataclass
333class AreaCharacter:
334    __slots__ = ("id", "name", "illustrator", "description1", "description2", "team", "props")
335
336    name: str
337    illustrator: str
338    description1: str
339    description2: str
340    team: str
341    props: dict[str, str]
342
343
344@dataclass
345class AreaSong:
346    __slots__ = ("id", "title", "artist", "description", "illustrator", "movie")
347
348    id: Optional[int]
349    title: str
350    artist: str
351    description: str
352    illustrator: Optional[str]
353    movie: Optional[str]
354
355
356@dataclass
357class Area:
358    __slots__ = ("id", "name", "comment", "description", "video_id", "characters", "songs")
359
360    id: str
361    name: str
362    comment: str
363    description: str
364    video_id: str
365    characters: list[AreaCharacter]
366    songs: list[AreaSong]
367
368
369@dataclass
370class Score:
371    __slots__ = ("id", "level", "level_index", "achievements", "fc", "fs", "dx_score", "dx_rating", "play_count", "rate", "type", "song")
372
373    id: int
374    level: str
375    level_index: LevelIndex
376    achievements: Optional[float]
377    fc: Optional[FCType]
378    fs: Optional[FSType]
379    dx_score: Optional[int]
380    dx_rating: Optional[float]
381    play_count: Optional[int]
382    rate: RateType
383    type: SongType
384
385    def _compare(self, other: Optional["Score"]) -> "Score":
386        if other is None:
387            return self
388        if self.dx_score != other.dx_score:  # larger value is better
389            return self if (self.dx_score or 0) > (other.dx_score or 0) else other
390        if self.achievements != other.achievements:  # larger value is better
391            return self if (self.achievements or 0) > (other.achievements or 0) else other
392        if self.rate != other.rate:  # smaller value is better
393            self_rate = self.rate.value if self.rate is not None else 100
394            other_rate = other.rate.value if other.rate is not None else 100
395            return self if self_rate < other_rate else other
396        if self.fc != other.fc:  # smaller value is better
397            self_fc = self.fc.value if self.fc is not None else 100
398            other_fc = other.fc.value if other.fc is not None else 100
399            return self if self_fc < other_fc else other
400        if self.fs != other.fs:  # bigger value is better
401            self_fs = self.fs.value if self.fs is not None else -1
402            other_fs = other.fs.value if other.fs is not None else -1
403            return self if self_fs > other_fs else other
404        return self  # we consider they are equal
405
406    def _join(self, other: Optional["Score"]) -> "Score":
407        if other is not None:
408            if self.level_index != other.level_index or self.type != other.type:
409                raise ValueError("Cannot join scores with different level indexes or types")
410            self.achievements = max(self.achievements or 0, other.achievements or 0)
411            if self.fc != other.fc:
412                self_fc = self.fc.value if self.fc is not None else 100
413                other_fc = other.fc.value if other.fc is not None else 100
414                selected_value = min(self_fc, other_fc)
415                self.fc = FCType(selected_value) if selected_value != 100 else None
416            if self.fs != other.fs:
417                self_fs = self.fs.value if self.fs is not None else -1
418                other_fs = other.fs.value if other.fs is not None else -1
419                selected_value = max(self_fs, other_fs)
420                self.fs = FSType(selected_value) if selected_value != -1 else None
421            if self.rate != other.rate:
422                selected_value = min(self.rate.value, other.rate.value)
423                self.rate = RateType(selected_value)
424        return self
425
426
427@dataclass
428class ScoreExtend(Score):
429    __slots__ = ["title", "level_value", "level_dx_score"]
430
431    title: str
432    level_value: float
433    level_dx_score: int
434
435
436@dataclass
437class PlateObject:
438    __slots__ = ("song", "levels", "scores")
439
440    song: Song
441    levels: set[LevelIndex]
442    scores: list[ScoreExtend]
443
444
445@dataclass
446class PlayerSong:
447    __slots__ = ["song", "scores"]
448
449    song: Song
450    scores: list[ScoreExtend]
451
452
453@dataclass
454class PlayerBests:
455    __slots__ = ["rating", "rating_b35", "rating_b15", "scores_b35", "scores_b15"]
456
457    rating: int
458    rating_b35: int
459    rating_b15: int
460    scores_b35: list[ScoreExtend]
461    scores_b15: list[ScoreExtend]
462
463    @property
464    def scores(self) -> list[ScoreExtend]:
465        """Get all scores, including both B35 and B15."""
466        return self.scores_b35 + self.scores_b15
@dataclass
class Song:
11@dataclass
12class Song:
13    __slots__ = ("id", "title", "artist", "genre", "bpm", "map", "version", "rights", "aliases", "disabled", "difficulties")
14
15    id: int
16    title: str
17    artist: str
18    genre: Genre
19    bpm: int
20    map: Optional[str]
21    version: int
22    rights: Optional[str]
23    aliases: Optional[list[str]]
24    disabled: bool
25    difficulties: "SongDifficulties"
26
27    def get_difficulty(self, type: SongType, level_index: LevelIndex) -> Optional["SongDifficulty"]:
28        """Get the exact difficulty of this song by type and level index.
29
30        Args:
31            type: The type of the song (DX, STANDARD, UTAGE).
32            level_index: The level index of the difficulty.
33        Returns:
34            The difficulty object if found, otherwise None.
35        """
36        if type == SongType.DX:
37            return next((diff for diff in self.difficulties.dx if diff.level_index == level_index), None)
38        if type == SongType.STANDARD:
39            return next((diff for diff in self.difficulties.standard if diff.level_index == level_index), None)
40        if type == SongType.UTAGE:
41            return next(iter(self.difficulties.utage), None)
42
43    def get_difficulties(self, song_type: Union[SongType, _UnsetSentinel] = UNSET) -> Sequence["SongDifficulty"]:
44        """Get all difficulties of the song, optionally filtered by type.
45
46        Args:
47            song_type: The type of the song (DX, STANDARD, UTAGE). If UNSET, returns all types.
48        Returns:
49            A sequence of difficulties matching the specified type.
50        """
51        if isinstance(song_type, _UnsetSentinel):
52            return self.difficulties.standard + self.difficulties.dx + self.difficulties.utage
53        if song_type == SongType.DX:
54            return self.difficulties.dx
55        if song_type == SongType.STANDARD:
56            return self.difficulties.standard
57        if song_type == SongType.UTAGE:
58            return self.difficulties.utage
59
60    def get_divingfish_id(self, type: SongType, level_index: LevelIndex) -> int:
61        """Get the Diving Fish ID for a specific difficulty of this song.
62
63        Args:
64            type: The type of the song (DX, STANDARD, UTAGE).
65            level_index: The level index of the difficulty.
66        Returns:
67            The Diving Fish ID for the specified difficulty.
68        """
69        difficulty = self.get_difficulty(type, level_index)
70        if difficulty is None:
71            raise ValueError(f"No difficulty found for type {type} and level index {level_index}")
72        if difficulty.type == SongType.DX:
73            return self.id + 10000
74        if difficulty.type == SongType.UTAGE:
75            return self.id + 100000
76        return self.id
77
78    def get_divingfish_ids(self, song_type: Union[SongType, _UnsetSentinel] = UNSET) -> set[int]:
79        """Get a set of Diving Fish IDs for all difficulties of this song.
80
81        Args:
82            song_type: The type of the song (DX, STANDARD, UTAGE). If UNSET, returns IDs for all types.
83        Returns:
84            A set of Diving Fish IDs for the specified type or all types if UNSET.
85        """
86        ids = set()
87        for difficulty in self.get_difficulties(song_type):
88            ids.add(self.get_divingfish_id(difficulty.type, difficulty.level_index))
89        return ids
Song( id: int, title: str, artist: str, genre: maimai_py.enums.Genre, bpm: int, map: Optional[str], version: int, rights: Optional[str], aliases: Optional[list[str]], disabled: bool, difficulties: SongDifficulties)
id: int
title: str
artist: str
bpm: int
map: Optional[str]
version: int
rights: Optional[str]
aliases: Optional[list[str]]
disabled: bool
difficulties: SongDifficulties
def get_difficulty( self, type: maimai_py.enums.SongType, level_index: maimai_py.enums.LevelIndex) -> Optional[SongDifficulty]:
27    def get_difficulty(self, type: SongType, level_index: LevelIndex) -> Optional["SongDifficulty"]:
28        """Get the exact difficulty of this song by type and level index.
29
30        Args:
31            type: The type of the song (DX, STANDARD, UTAGE).
32            level_index: The level index of the difficulty.
33        Returns:
34            The difficulty object if found, otherwise None.
35        """
36        if type == SongType.DX:
37            return next((diff for diff in self.difficulties.dx if diff.level_index == level_index), None)
38        if type == SongType.STANDARD:
39            return next((diff for diff in self.difficulties.standard if diff.level_index == level_index), None)
40        if type == SongType.UTAGE:
41            return next(iter(self.difficulties.utage), None)

Get the exact difficulty of this song by type and level index.

Arguments:
  • type: The type of the song (DX, STANDARD, UTAGE).
  • level_index: The level index of the difficulty.
Returns:

The difficulty object if found, otherwise None.

def get_difficulties( self, song_type: Union[maimai_py.enums.SongType, maimai_py.utils.sentinel._UnsetSentinel] = Unset) -> Sequence[SongDifficulty]:
43    def get_difficulties(self, song_type: Union[SongType, _UnsetSentinel] = UNSET) -> Sequence["SongDifficulty"]:
44        """Get all difficulties of the song, optionally filtered by type.
45
46        Args:
47            song_type: The type of the song (DX, STANDARD, UTAGE). If UNSET, returns all types.
48        Returns:
49            A sequence of difficulties matching the specified type.
50        """
51        if isinstance(song_type, _UnsetSentinel):
52            return self.difficulties.standard + self.difficulties.dx + self.difficulties.utage
53        if song_type == SongType.DX:
54            return self.difficulties.dx
55        if song_type == SongType.STANDARD:
56            return self.difficulties.standard
57        if song_type == SongType.UTAGE:
58            return self.difficulties.utage

Get all difficulties of the song, optionally filtered by type.

Arguments:
  • song_type: The type of the song (DX, STANDARD, UTAGE). If UNSET, returns all types.
Returns:

A sequence of difficulties matching the specified type.

def get_divingfish_id( self, type: maimai_py.enums.SongType, level_index: maimai_py.enums.LevelIndex) -> int:
60    def get_divingfish_id(self, type: SongType, level_index: LevelIndex) -> int:
61        """Get the Diving Fish ID for a specific difficulty of this song.
62
63        Args:
64            type: The type of the song (DX, STANDARD, UTAGE).
65            level_index: The level index of the difficulty.
66        Returns:
67            The Diving Fish ID for the specified difficulty.
68        """
69        difficulty = self.get_difficulty(type, level_index)
70        if difficulty is None:
71            raise ValueError(f"No difficulty found for type {type} and level index {level_index}")
72        if difficulty.type == SongType.DX:
73            return self.id + 10000
74        if difficulty.type == SongType.UTAGE:
75            return self.id + 100000
76        return self.id

Get the Diving Fish ID for a specific difficulty of this song.

Arguments:
  • type: The type of the song (DX, STANDARD, UTAGE).
  • level_index: The level index of the difficulty.
Returns:

The Diving Fish ID for the specified difficulty.

def get_divingfish_ids( self, song_type: Union[maimai_py.enums.SongType, maimai_py.utils.sentinel._UnsetSentinel] = Unset) -> set[int]:
78    def get_divingfish_ids(self, song_type: Union[SongType, _UnsetSentinel] = UNSET) -> set[int]:
79        """Get a set of Diving Fish IDs for all difficulties of this song.
80
81        Args:
82            song_type: The type of the song (DX, STANDARD, UTAGE). If UNSET, returns IDs for all types.
83        Returns:
84            A set of Diving Fish IDs for the specified type or all types if UNSET.
85        """
86        ids = set()
87        for difficulty in self.get_difficulties(song_type):
88            ids.add(self.get_divingfish_id(difficulty.type, difficulty.level_index))
89        return ids

Get a set of Diving Fish IDs for all difficulties of this song.

Arguments:
  • song_type: The type of the song (DX, STANDARD, UTAGE). If UNSET, returns IDs for all types.
Returns:

A set of Diving Fish IDs for the specified type or all types if UNSET.

@dataclass
class SongDifficulties:
92@dataclass
93class SongDifficulties:
94    __slots__ = ("standard", "dx", "utage")
95
96    standard: list["SongDifficulty"]
97    dx: list["SongDifficulty"]
98    utage: list["SongDifficultyUtage"]
SongDifficulties( standard: list[SongDifficulty], dx: list[SongDifficulty], utage: list[SongDifficultyUtage])
standard: list[SongDifficulty]
dx: list[SongDifficulty]
utage: list[SongDifficultyUtage]
@dataclass
class CurveObject:
101@dataclass
102class CurveObject:
103    __slots__ = ("sample_size", "fit_level_value", "avg_achievements", "stdev_achievements", "avg_dx_score", "rate_sample_size", "fc_sample_size")
104
105    sample_size: int
106    fit_level_value: float
107    avg_achievements: float
108    stdev_achievements: float
109    avg_dx_score: float
110    rate_sample_size: dict[RateType, int]
111    fc_sample_size: dict[FCType, int]
CurveObject( sample_size: int, fit_level_value: float, avg_achievements: float, stdev_achievements: float, avg_dx_score: float, rate_sample_size: dict[maimai_py.enums.RateType, int], fc_sample_size: dict[maimai_py.enums.FCType, int])
sample_size: int
fit_level_value: float
avg_achievements: float
stdev_achievements: float
avg_dx_score: float
rate_sample_size: dict[maimai_py.enums.RateType, int]
fc_sample_size: dict[maimai_py.enums.FCType, int]
@dataclass
class SongDifficulty:
114@dataclass
115class SongDifficulty:
116    __slots__ = (
117        "id",
118        "type",
119        "level",
120        "level_value",
121        "level_index",
122        "note_designer",
123        "version",
124        "tap_num",
125        "hold_num",
126        "slide_num",
127        "touch_num",
128        "break_num",
129        "curve",
130    )
131
132    type: SongType
133    level: str
134    level_value: float
135    level_index: LevelIndex
136    note_designer: str
137    version: int
138    tap_num: int
139    hold_num: int
140    slide_num: int
141    touch_num: int
142    break_num: int
143    curve: Optional[CurveObject]
SongDifficulty( type: maimai_py.enums.SongType, level: str, level_value: float, level_index: maimai_py.enums.LevelIndex, note_designer: str, version: int, tap_num: int, hold_num: int, slide_num: int, touch_num: int, break_num: int, curve: Optional[CurveObject])
level: str
level_value: float
note_designer: str
version: int
tap_num: int
hold_num: int
slide_num: int
touch_num: int
break_num: int
curve: Optional[CurveObject]
id
@dataclass
class SongDifficultyUtage(SongDifficulty):
146@dataclass
147class SongDifficultyUtage(SongDifficulty):
148    __slots__ = ("kanji", "description", "is_buddy")
149
150    kanji: str
151    description: str
152    is_buddy: bool
SongDifficultyUtage( type: maimai_py.enums.SongType, level: str, level_value: float, level_index: maimai_py.enums.LevelIndex, note_designer: str, version: int, tap_num: int, hold_num: int, slide_num: int, touch_num: int, break_num: int, curve: Optional[CurveObject], kanji: str, description: str, is_buddy: bool)
kanji: str
description: str
is_buddy: bool
@dataclass
class SongAlias:
155@dataclass
156class SongAlias:
157    __slots__ = ("song_id", "aliases")
158
159    song_id: int
160    aliases: list[str]
SongAlias(song_id: int, aliases: list[str])
song_id: int
aliases: list[str]
@dataclass
class PlayerIdentifier:
163@dataclass
164class PlayerIdentifier:
165    qq: Optional[int] = None
166    username: Optional[str] = None
167    friend_code: Optional[int] = None
168    credentials: Union[str, MutableMapping[str, Any], None] = None
169
170    def __post_init__(self):
171        if self.qq is None and self.username is None and self.friend_code is None and self.credentials is None:
172            raise InvalidPlayerIdentifierError("At least one of the following must be provided: qq, username, friend_code, credentials")
173
174    def _as_diving_fish(self) -> dict[str, Any]:
175        if self.qq:
176            return {"qq": str(self.qq)}
177        elif self.username:
178            return {"username": self.username}
179        elif self.friend_code:
180            raise InvalidPlayerIdentifierError("Friend code is not applicable for Diving Fish")
181        else:
182            raise InvalidPlayerIdentifierError("No valid identifier provided")
183
184    def _as_lxns(self) -> str:
185        if self.friend_code:
186            return str(self.friend_code)
187        elif self.qq:
188            return f"qq/{str(self.qq)}"
189        elif self.username:
190            raise InvalidPlayerIdentifierError("Username is not applicable for LXNS")
191        else:
192            raise InvalidPlayerIdentifierError("No valid identifier provided")
PlayerIdentifier( qq: Optional[int] = None, username: Optional[str] = None, friend_code: Optional[int] = None, credentials: Union[str, MutableMapping[str, Any], NoneType] = None)
qq: Optional[int] = None
username: Optional[str] = None
friend_code: Optional[int] = None
credentials: Union[str, MutableMapping[str, Any], NoneType] = None
@dataclass
class PlayerItem:
195@dataclass
196class PlayerItem:
197    @staticmethod
198    def _namespace() -> str:
199        raise NotImplementedError
@dataclass
class PlayerTrophy(PlayerItem):
202@dataclass
203class PlayerTrophy(PlayerItem):
204    __slots__ = ("id", "name", "color")
205
206    id: int
207    name: str
208    color: str
209
210    @staticmethod
211    def _namespace():
212        return "trophies"
PlayerTrophy(id: int, name: str, color: str)
id: int
name: str
color: str
@dataclass
class PlayerIcon(PlayerItem):
215@dataclass
216class PlayerIcon(PlayerItem):
217    __slots__ = ("id", "name", "description", "genre")
218
219    id: int
220    name: str
221    description: Optional[str]
222    genre: Optional[str]
223
224    @staticmethod
225    def _namespace():
226        return "icons"
PlayerIcon(id: int, name: str, description: Optional[str], genre: Optional[str])
id: int
name: str
description: Optional[str]
genre: Optional[str]
@dataclass
class PlayerNamePlate(PlayerItem):
229@dataclass
230class PlayerNamePlate(PlayerItem):
231    __slots__ = ("id", "name", "description", "genre")
232
233    id: int
234    name: str
235    description: Optional[str]
236    genre: Optional[str]
237
238    @staticmethod
239    def _namespace():
240        return "nameplates"
PlayerNamePlate(id: int, name: str, description: Optional[str], genre: Optional[str])
id: int
name: str
description: Optional[str]
genre: Optional[str]
@dataclass
class PlayerFrame(PlayerItem):
243@dataclass
244class PlayerFrame(PlayerItem):
245    __slots__ = ("id", "name", "description", "genre")
246
247    id: int
248    name: str
249    description: Optional[str]
250    genre: Optional[str]
251
252    @staticmethod
253    def _namespace():
254        return "frames"
PlayerFrame(id: int, name: str, description: Optional[str], genre: Optional[str])
id: int
name: str
description: Optional[str]
genre: Optional[str]
@dataclass
class PlayerPartner(PlayerItem):
257@dataclass
258class PlayerPartner(PlayerItem):
259    __slots__ = ("id", "name")
260
261    id: int
262    name: str
263
264    @staticmethod
265    def _namespace():
266        return "partners"
PlayerPartner(id: int, name: str)
id: int
name: str
@dataclass
class PlayerChara(PlayerItem):
269@dataclass
270class PlayerChara(PlayerItem):
271    __slots__ = ("id", "name")
272
273    id: int
274    name: str
275
276    @staticmethod
277    def _namespace():
278        return "charas"
PlayerChara(id: int, name: str)
id: int
name: str
@dataclass
class PlayerRegion:
281@dataclass
282class PlayerRegion:
283    __slots__ = ("region_id", "region_name", "play_count", "created_at")
284
285    region_id: int
286    region_name: str
287    play_count: int
288    created_at: datetime
PlayerRegion( region_id: int, region_name: str, play_count: int, created_at: datetime.datetime)
region_id: int
region_name: str
play_count: int
created_at: datetime.datetime
@dataclass
class Player:
291@dataclass
292class Player:
293    __slots__ = ("identifier", "name", "rating")
294
295    name: str
296    rating: int
Player(name: str, rating: int)
name: str
rating: int
identifier
@dataclass
class DivingFishPlayer(Player):
299@dataclass
300class DivingFishPlayer(Player):
301    __slots__ = ("nickname", "plate", "additional_rating")
302
303    nickname: str
304    plate: str
305    additional_rating: int
DivingFishPlayer( name: str, rating: int, nickname: str, plate: str, additional_rating: int)
nickname: str
plate: str
additional_rating: int
Inherited Members
Player
name
rating
identifier
@dataclass
class LXNSPlayer(Player):
308@dataclass
309class LXNSPlayer(Player):
310    __slots__ = ("friend_code", "course_rank", "class_rank", "star", "frame", "icon", "trophy", "name_plate", "upload_time")
311
312    friend_code: int
313    course_rank: int
314    class_rank: int
315    star: int
316    frame: Optional[PlayerFrame]
317    icon: Optional[PlayerIcon]
318    trophy: Optional[PlayerTrophy]
319    name_plate: Optional[PlayerNamePlate]
320    upload_time: str
LXNSPlayer( name: str, rating: int, friend_code: int, course_rank: int, class_rank: int, star: int, frame: Optional[PlayerFrame], icon: Optional[PlayerIcon], trophy: Optional[PlayerTrophy], name_plate: Optional[PlayerNamePlate], upload_time: str)
friend_code: int
course_rank: int
class_rank: int
star: int
frame: Optional[PlayerFrame]
icon: Optional[PlayerIcon]
trophy: Optional[PlayerTrophy]
name_plate: Optional[PlayerNamePlate]
upload_time: str
Inherited Members
Player
name
rating
identifier
@dataclass
class ArcadePlayer(Player):
323@dataclass
324class ArcadePlayer(Player):
325    __slots__ = ("is_login", "icon", "trophy", "name_plate")
326
327    is_login: bool
328    icon: Optional[PlayerIcon]
329    trophy: Optional[PlayerTrophy]
330    name_plate: Optional[PlayerNamePlate]
ArcadePlayer( name: str, rating: int, is_login: bool, icon: Optional[PlayerIcon], trophy: Optional[PlayerTrophy], name_plate: Optional[PlayerNamePlate])
is_login: bool
icon: Optional[PlayerIcon]
trophy: Optional[PlayerTrophy]
name_plate: Optional[PlayerNamePlate]
Inherited Members
Player
name
rating
identifier
@dataclass
class AreaCharacter:
333@dataclass
334class AreaCharacter:
335    __slots__ = ("id", "name", "illustrator", "description1", "description2", "team", "props")
336
337    name: str
338    illustrator: str
339    description1: str
340    description2: str
341    team: str
342    props: dict[str, str]
AreaCharacter( name: str, illustrator: str, description1: str, description2: str, team: str, props: dict[str, str])
name: str
illustrator: str
description1: str
description2: str
team: str
props: dict[str, str]
id
@dataclass
class AreaSong:
345@dataclass
346class AreaSong:
347    __slots__ = ("id", "title", "artist", "description", "illustrator", "movie")
348
349    id: Optional[int]
350    title: str
351    artist: str
352    description: str
353    illustrator: Optional[str]
354    movie: Optional[str]
AreaSong( id: Optional[int], title: str, artist: str, description: str, illustrator: Optional[str], movie: Optional[str])
id: Optional[int]
title: str
artist: str
description: str
illustrator: Optional[str]
movie: Optional[str]
@dataclass
class Area:
357@dataclass
358class Area:
359    __slots__ = ("id", "name", "comment", "description", "video_id", "characters", "songs")
360
361    id: str
362    name: str
363    comment: str
364    description: str
365    video_id: str
366    characters: list[AreaCharacter]
367    songs: list[AreaSong]
Area( id: str, name: str, comment: str, description: str, video_id: str, characters: list[AreaCharacter], songs: list[AreaSong])
id: str
name: str
comment: str
description: str
video_id: str
characters: list[AreaCharacter]
songs: list[AreaSong]
@dataclass
class Score:
370@dataclass
371class Score:
372    __slots__ = ("id", "level", "level_index", "achievements", "fc", "fs", "dx_score", "dx_rating", "play_count", "rate", "type", "song")
373
374    id: int
375    level: str
376    level_index: LevelIndex
377    achievements: Optional[float]
378    fc: Optional[FCType]
379    fs: Optional[FSType]
380    dx_score: Optional[int]
381    dx_rating: Optional[float]
382    play_count: Optional[int]
383    rate: RateType
384    type: SongType
385
386    def _compare(self, other: Optional["Score"]) -> "Score":
387        if other is None:
388            return self
389        if self.dx_score != other.dx_score:  # larger value is better
390            return self if (self.dx_score or 0) > (other.dx_score or 0) else other
391        if self.achievements != other.achievements:  # larger value is better
392            return self if (self.achievements or 0) > (other.achievements or 0) else other
393        if self.rate != other.rate:  # smaller value is better
394            self_rate = self.rate.value if self.rate is not None else 100
395            other_rate = other.rate.value if other.rate is not None else 100
396            return self if self_rate < other_rate else other
397        if self.fc != other.fc:  # smaller value is better
398            self_fc = self.fc.value if self.fc is not None else 100
399            other_fc = other.fc.value if other.fc is not None else 100
400            return self if self_fc < other_fc else other
401        if self.fs != other.fs:  # bigger value is better
402            self_fs = self.fs.value if self.fs is not None else -1
403            other_fs = other.fs.value if other.fs is not None else -1
404            return self if self_fs > other_fs else other
405        return self  # we consider they are equal
406
407    def _join(self, other: Optional["Score"]) -> "Score":
408        if other is not None:
409            if self.level_index != other.level_index or self.type != other.type:
410                raise ValueError("Cannot join scores with different level indexes or types")
411            self.achievements = max(self.achievements or 0, other.achievements or 0)
412            if self.fc != other.fc:
413                self_fc = self.fc.value if self.fc is not None else 100
414                other_fc = other.fc.value if other.fc is not None else 100
415                selected_value = min(self_fc, other_fc)
416                self.fc = FCType(selected_value) if selected_value != 100 else None
417            if self.fs != other.fs:
418                self_fs = self.fs.value if self.fs is not None else -1
419                other_fs = other.fs.value if other.fs is not None else -1
420                selected_value = max(self_fs, other_fs)
421                self.fs = FSType(selected_value) if selected_value != -1 else None
422            if self.rate != other.rate:
423                selected_value = min(self.rate.value, other.rate.value)
424                self.rate = RateType(selected_value)
425        return self
Score( id: int, level: str, level_index: maimai_py.enums.LevelIndex, achievements: Optional[float], fc: Optional[maimai_py.enums.FCType], fs: Optional[maimai_py.enums.FSType], dx_score: Optional[int], dx_rating: Optional[float], play_count: Optional[int], rate: maimai_py.enums.RateType, type: maimai_py.enums.SongType)
id: int
level: str
achievements: Optional[float]
fc: Optional[maimai_py.enums.FCType]
fs: Optional[maimai_py.enums.FSType]
dx_score: Optional[int]
dx_rating: Optional[float]
play_count: Optional[int]
song
@dataclass
class ScoreExtend(Score):
428@dataclass
429class ScoreExtend(Score):
430    __slots__ = ["title", "level_value", "level_dx_score"]
431
432    title: str
433    level_value: float
434    level_dx_score: int
ScoreExtend( id: int, level: str, level_index: maimai_py.enums.LevelIndex, achievements: Optional[float], fc: Optional[maimai_py.enums.FCType], fs: Optional[maimai_py.enums.FSType], dx_score: Optional[int], dx_rating: Optional[float], play_count: Optional[int], rate: maimai_py.enums.RateType, type: maimai_py.enums.SongType, title: str, level_value: float, level_dx_score: int)
title: str
level_value: float
level_dx_score: int
@dataclass
class PlateObject:
437@dataclass
438class PlateObject:
439    __slots__ = ("song", "levels", "scores")
440
441    song: Song
442    levels: set[LevelIndex]
443    scores: list[ScoreExtend]
PlateObject( song: Song, levels: set[maimai_py.enums.LevelIndex], scores: list[ScoreExtend])
song: Song
scores: list[ScoreExtend]
@dataclass
class PlayerSong:
446@dataclass
447class PlayerSong:
448    __slots__ = ["song", "scores"]
449
450    song: Song
451    scores: list[ScoreExtend]
PlayerSong( song: Song, scores: list[ScoreExtend])
song: Song
scores: list[ScoreExtend]
@dataclass
class PlayerBests:
454@dataclass
455class PlayerBests:
456    __slots__ = ["rating", "rating_b35", "rating_b15", "scores_b35", "scores_b15"]
457
458    rating: int
459    rating_b35: int
460    rating_b15: int
461    scores_b35: list[ScoreExtend]
462    scores_b15: list[ScoreExtend]
463
464    @property
465    def scores(self) -> list[ScoreExtend]:
466        """Get all scores, including both B35 and B15."""
467        return self.scores_b35 + self.scores_b15
PlayerBests( rating: int, rating_b35: int, rating_b15: int, scores_b35: list[ScoreExtend], scores_b15: list[ScoreExtend])
rating: int
rating_b35: int
rating_b15: int
scores_b35: list[ScoreExtend]
scores_b15: list[ScoreExtend]
scores: list[ScoreExtend]
464    @property
465    def scores(self) -> list[ScoreExtend]:
466        """Get all scores, including both B35 and B15."""
467        return self.scores_b35 + self.scores_b15

Get all scores, including both B35 and B15.