mirror of
https://github.com/283375/arcaea-offline.git
synced 2025-07-01 12:16:26 +00:00
@ -1,38 +1,32 @@
|
|||||||
from .arcaea import (
|
from ._base import ModelBase, ModelViewBase
|
||||||
Chart,
|
from .chart_info import ChartInfo
|
||||||
ChartInfo,
|
|
||||||
Difficulty,
|
|
||||||
DifficultyLocalized,
|
|
||||||
Pack,
|
|
||||||
PackLocalized,
|
|
||||||
Song,
|
|
||||||
SongLocalized,
|
|
||||||
SongSearchWord,
|
|
||||||
)
|
|
||||||
from .base import ModelsV5Base, ModelsV5ViewBase
|
|
||||||
from .config import Property
|
from .config import Property
|
||||||
from .play_results import (
|
from .difficulty import Difficulty, DifficultyLocalization
|
||||||
|
from .pack import Pack, PackLocalization
|
||||||
|
from .song import Song, SongLocalization
|
||||||
|
|
||||||
|
from .chart import Chart # isort: skip
|
||||||
|
from .play_result import (
|
||||||
CalculatedPotential,
|
CalculatedPotential,
|
||||||
PlayResult,
|
PlayResult,
|
||||||
PlayResultBest,
|
PlayResultBest,
|
||||||
PlayResultCalculated,
|
PlayResultCalculated,
|
||||||
)
|
) # isort: skip
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CalculatedPotential",
|
"CalculatedPotential",
|
||||||
"Chart",
|
"Chart",
|
||||||
"ChartInfo",
|
"ChartInfo",
|
||||||
"Difficulty",
|
"Difficulty",
|
||||||
"DifficultyLocalized",
|
"DifficultyLocalization",
|
||||||
"ModelsV5Base",
|
"ModelBase",
|
||||||
"ModelsV5ViewBase",
|
"ModelViewBase",
|
||||||
"Pack",
|
"Pack",
|
||||||
"PackLocalized",
|
"PackLocalization",
|
||||||
"PlayResult",
|
"PlayResult",
|
||||||
"PlayResultBest",
|
"PlayResultBest",
|
||||||
"PlayResultCalculated",
|
"PlayResultCalculated",
|
||||||
"Property",
|
"Property",
|
||||||
"Song",
|
"Song",
|
||||||
"SongLocalized",
|
"SongLocalization",
|
||||||
"SongSearchWord",
|
|
||||||
]
|
]
|
||||||
|
@ -1,31 +1,30 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import MetaData
|
||||||
from sqlalchemy.orm import DeclarativeBase
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
from sqlalchemy.orm.exc import DetachedInstanceError
|
from sqlalchemy.orm.exc import DetachedInstanceError
|
||||||
|
|
||||||
from arcaea_offline.constants.enums import (
|
from ._types import ForceTimezoneDateTime
|
||||||
ArcaeaPlayResultClearType,
|
|
||||||
ArcaeaPlayResultModifier,
|
|
||||||
ArcaeaRatingClass,
|
|
||||||
ArcaeaSongSide,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ._custom_types import DbIntEnum, TZDateTime
|
|
||||||
|
|
||||||
TYPE_ANNOTATION_MAP = {
|
TYPE_ANNOTATION_MAP = {
|
||||||
datetime: TZDateTime,
|
datetime: ForceTimezoneDateTime,
|
||||||
ArcaeaRatingClass: DbIntEnum(ArcaeaRatingClass),
|
|
||||||
ArcaeaSongSide: DbIntEnum(ArcaeaSongSide),
|
|
||||||
ArcaeaPlayResultClearType: DbIntEnum(ArcaeaPlayResultClearType),
|
|
||||||
ArcaeaPlayResultModifier: DbIntEnum(ArcaeaPlayResultModifier),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ModelsV5Base(DeclarativeBase):
|
class ModelBase(DeclarativeBase):
|
||||||
type_annotation_map = TYPE_ANNOTATION_MAP
|
type_annotation_map = TYPE_ANNOTATION_MAP
|
||||||
|
metadata = MetaData(
|
||||||
|
naming_convention={
|
||||||
|
"ix": "ix_%(column_0_label)s",
|
||||||
|
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
||||||
|
"ck": "ck_%(table_name)s_`%(constraint_name)s`",
|
||||||
|
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
||||||
|
"pk": "pk_%(table_name)s",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModelsV5ViewBase(DeclarativeBase):
|
class ModelViewBase(DeclarativeBase):
|
||||||
type_annotation_map = TYPE_ANNOTATION_MAP
|
type_annotation_map = TYPE_ANNOTATION_MAP
|
||||||
|
|
||||||
|
|
||||||
@ -34,7 +33,7 @@ class ReprHelper:
|
|||||||
|
|
||||||
def _repr(self, **kwargs) -> str:
|
def _repr(self, **kwargs) -> str:
|
||||||
"""
|
"""
|
||||||
Helper for __repr__
|
SQLAlchemy model __repr__ helper
|
||||||
|
|
||||||
https://stackoverflow.com/a/55749579/16484891
|
https://stackoverflow.com/a/55749579/16484891
|
||||||
|
|
@ -1,46 +0,0 @@
|
|||||||
from datetime import datetime, timezone
|
|
||||||
from enum import IntEnum
|
|
||||||
from typing import Optional, Type
|
|
||||||
|
|
||||||
from sqlalchemy import DateTime, Integer
|
|
||||||
from sqlalchemy.types import TypeDecorator
|
|
||||||
|
|
||||||
|
|
||||||
class DbIntEnum(TypeDecorator):
|
|
||||||
"""sqlalchemy `TypeDecorator` for `IntEnum`s"""
|
|
||||||
|
|
||||||
impl = Integer
|
|
||||||
cache_ok = True
|
|
||||||
|
|
||||||
def __init__(self, enum_class: Type[IntEnum]):
|
|
||||||
super().__init__()
|
|
||||||
self.enum_class = enum_class
|
|
||||||
|
|
||||||
def process_bind_param(self, value: Optional[IntEnum], dialect) -> Optional[int]:
|
|
||||||
return None if value is None else value.value
|
|
||||||
|
|
||||||
def process_result_value(self, value: Optional[int], dialect) -> Optional[IntEnum]:
|
|
||||||
return None if value is None else self.enum_class(value)
|
|
||||||
|
|
||||||
|
|
||||||
class TZDateTime(TypeDecorator):
|
|
||||||
"""
|
|
||||||
Store Timezone Aware Timestamps as Timezone Naive UTC
|
|
||||||
|
|
||||||
https://docs.sqlalchemy.org/en/20/core/custom_types.html#store-timezone-aware-timestamps-as-timezone-naive-utc
|
|
||||||
"""
|
|
||||||
|
|
||||||
impl = DateTime
|
|
||||||
cache_ok = True
|
|
||||||
|
|
||||||
def process_bind_param(self, value: Optional[datetime], dialect):
|
|
||||||
if value is not None:
|
|
||||||
if not value.tzinfo or value.tzinfo.utcoffset(value) is None:
|
|
||||||
raise TypeError("tzinfo is required")
|
|
||||||
value = value.astimezone(timezone.utc).replace(tzinfo=None)
|
|
||||||
return value
|
|
||||||
|
|
||||||
def process_result_value(self, value: Optional[datetime], dialect):
|
|
||||||
if value is not None:
|
|
||||||
value = value.replace(tzinfo=timezone.utc)
|
|
||||||
return value
|
|
28
src/arcaea_offline/database/models/_types.py
Normal file
28
src/arcaea_offline/database/models/_types.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime
|
||||||
|
from sqlalchemy.types import TypeDecorator
|
||||||
|
|
||||||
|
|
||||||
|
class ForceTimezoneDateTime(TypeDecorator):
|
||||||
|
"""
|
||||||
|
Store timezone aware timestamps as timezone naive UTC
|
||||||
|
|
||||||
|
https://docs.sqlalchemy.org/en/20/core/custom_types.html#store-timezone-aware-timestamps-as-timezone-naive-utc
|
||||||
|
"""
|
||||||
|
|
||||||
|
impl = DateTime
|
||||||
|
cache_ok = True
|
||||||
|
|
||||||
|
def process_bind_param(self, value: Optional[datetime], dialect):
|
||||||
|
if value is not None:
|
||||||
|
if not value.tzinfo or value.tzinfo.utcoffset(value) is None:
|
||||||
|
raise TypeError("datetime tzinfo is required")
|
||||||
|
value = value.astimezone(timezone.utc).replace(tzinfo=None)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def process_result_value(self, value: Optional[datetime], dialect):
|
||||||
|
if value is not None:
|
||||||
|
value = value.replace(tzinfo=timezone.utc)
|
||||||
|
return value
|
@ -1,285 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from sqlalchemy import Enum, ForeignKey, and_, func, select
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
from sqlalchemy_utils import create_view
|
|
||||||
|
|
||||||
from arcaea_offline.constants.enums.arcaea import (
|
|
||||||
ArcaeaLanguage,
|
|
||||||
ArcaeaRatingClass,
|
|
||||||
ArcaeaSongSide,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .base import ModelsV5Base, ModelsV5ViewBase, ReprHelper
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"Chart",
|
|
||||||
"ChartInfo",
|
|
||||||
"Difficulty",
|
|
||||||
"DifficultyLocalized",
|
|
||||||
"Pack",
|
|
||||||
"PackLocalized",
|
|
||||||
"Song",
|
|
||||||
"SongLocalized",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
_ArcaeaLanguageEnumType = Enum(
|
|
||||||
ArcaeaLanguage, native_enum=False, values_callable=lambda e: [x.value for x in e]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Pack(ModelsV5Base, ReprHelper):
|
|
||||||
__tablename__ = "packs"
|
|
||||||
|
|
||||||
id: Mapped[str] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str]
|
|
||||||
description: Mapped[Optional[str]]
|
|
||||||
|
|
||||||
songs: Mapped[List["Song"]] = relationship(back_populates="pack", viewonly=True)
|
|
||||||
localized_objects: Mapped[List["PackLocalized"]] = relationship(
|
|
||||||
back_populates="parent", viewonly=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PackLocalized(ModelsV5Base, ReprHelper):
|
|
||||||
__tablename__ = "packs_localized"
|
|
||||||
|
|
||||||
pkid: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
id: Mapped[str] = mapped_column(
|
|
||||||
ForeignKey(Pack.id, onupdate="CASCADE", ondelete="NO ACTION")
|
|
||||||
)
|
|
||||||
lang: Mapped[ArcaeaLanguage] = mapped_column(_ArcaeaLanguageEnumType)
|
|
||||||
name: Mapped[Optional[str]]
|
|
||||||
description: Mapped[Optional[str]]
|
|
||||||
|
|
||||||
parent: Mapped[Pack] = relationship(viewonly=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Song(ModelsV5Base, ReprHelper):
|
|
||||||
__tablename__ = "songs"
|
|
||||||
|
|
||||||
idx: Mapped[int]
|
|
||||||
id: Mapped[str] = mapped_column(primary_key=True)
|
|
||||||
title: Mapped[str]
|
|
||||||
artist: Mapped[str]
|
|
||||||
pack_id: Mapped[str] = mapped_column(
|
|
||||||
ForeignKey(Pack.id, onupdate="CASCADE", ondelete="NO ACTION")
|
|
||||||
)
|
|
||||||
bpm: Mapped[Optional[str]]
|
|
||||||
bpm_base: Mapped[Optional[float]]
|
|
||||||
audio_preview: Mapped[Optional[int]]
|
|
||||||
audio_preview_end: Mapped[Optional[int]]
|
|
||||||
side: Mapped[Optional[ArcaeaSongSide]]
|
|
||||||
version: Mapped[Optional[str]]
|
|
||||||
date: Mapped[Optional[datetime]]
|
|
||||||
bg: Mapped[Optional[str]]
|
|
||||||
bg_inverse: Mapped[Optional[str]]
|
|
||||||
bg_day: Mapped[Optional[str]]
|
|
||||||
bg_night: Mapped[Optional[str]]
|
|
||||||
source: Mapped[Optional[str]]
|
|
||||||
source_copyright: Mapped[Optional[str]]
|
|
||||||
|
|
||||||
pack: Mapped[Pack] = relationship(viewonly=True)
|
|
||||||
difficulties: Mapped[List["Difficulty"]] = relationship(
|
|
||||||
back_populates="song", viewonly=True
|
|
||||||
)
|
|
||||||
localized_objects: Mapped[List["SongLocalized"]] = relationship(
|
|
||||||
back_populates="parent", viewonly=True
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def charts_info(self):
|
|
||||||
return [d.chart_info for d in self.difficulties if d.chart_info is not None]
|
|
||||||
|
|
||||||
|
|
||||||
class SongLocalized(ModelsV5Base, ReprHelper):
|
|
||||||
__tablename__ = "songs_localized"
|
|
||||||
|
|
||||||
pkid: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
id: Mapped[str] = mapped_column(
|
|
||||||
ForeignKey(Song.id, onupdate="CASCADE", ondelete="NO ACTION")
|
|
||||||
)
|
|
||||||
lang: Mapped[ArcaeaLanguage] = mapped_column(_ArcaeaLanguageEnumType)
|
|
||||||
title: Mapped[Optional[str]]
|
|
||||||
source: Mapped[Optional[str]]
|
|
||||||
|
|
||||||
parent: Mapped[Song] = relationship(
|
|
||||||
back_populates="localized_objects", viewonly=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SongSearchWord(ModelsV5Base, ReprHelper):
|
|
||||||
__tablename__ = "songs_search_words"
|
|
||||||
|
|
||||||
pkid: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
id: Mapped[str] = mapped_column(
|
|
||||||
ForeignKey(Song.id, onupdate="CASCADE", ondelete="NO ACTION")
|
|
||||||
)
|
|
||||||
lang: Mapped[ArcaeaLanguage] = mapped_column(_ArcaeaLanguageEnumType)
|
|
||||||
type: Mapped[int] = mapped_column(comment="1: title, 2: artist")
|
|
||||||
value: Mapped[str]
|
|
||||||
|
|
||||||
|
|
||||||
class Difficulty(ModelsV5Base, ReprHelper):
|
|
||||||
__tablename__ = "difficulties"
|
|
||||||
|
|
||||||
song_id: Mapped[str] = mapped_column(
|
|
||||||
ForeignKey(Song.id, onupdate="CASCADE", ondelete="NO ACTION"),
|
|
||||||
primary_key=True,
|
|
||||||
)
|
|
||||||
rating_class: Mapped[ArcaeaRatingClass] = mapped_column(primary_key=True)
|
|
||||||
rating: Mapped[int]
|
|
||||||
rating_plus: Mapped[bool]
|
|
||||||
chart_designer: Mapped[Optional[str]]
|
|
||||||
jacket_desginer: Mapped[Optional[str]]
|
|
||||||
audio_override: Mapped[bool] = mapped_column(default=False)
|
|
||||||
jacket_override: Mapped[bool] = mapped_column(default=False)
|
|
||||||
jacket_night: Mapped[Optional[str]]
|
|
||||||
title: Mapped[Optional[str]]
|
|
||||||
artist: Mapped[Optional[str]]
|
|
||||||
bg: Mapped[Optional[str]]
|
|
||||||
bg_inverse: Mapped[Optional[str]]
|
|
||||||
bpm: Mapped[Optional[str]]
|
|
||||||
bpm_base: Mapped[Optional[float]]
|
|
||||||
version: Mapped[Optional[str]]
|
|
||||||
date: Mapped[Optional[datetime]]
|
|
||||||
|
|
||||||
song: Mapped[Song] = relationship(back_populates="difficulties", viewonly=True)
|
|
||||||
chart_info: Mapped[Optional["ChartInfo"]] = relationship(
|
|
||||||
primaryjoin=(
|
|
||||||
"and_(Difficulty.song_id==ChartInfo.song_id, "
|
|
||||||
"Difficulty.rating_class==ChartInfo.rating_class)"
|
|
||||||
),
|
|
||||||
viewonly=True,
|
|
||||||
)
|
|
||||||
localized_objects: Mapped[List["DifficultyLocalized"]] = relationship(
|
|
||||||
primaryjoin=(
|
|
||||||
"and_(Difficulty.song_id==DifficultyLocalized.song_id, "
|
|
||||||
"Difficulty.rating_class==DifficultyLocalized.rating_class)"
|
|
||||||
),
|
|
||||||
viewonly=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DifficultyLocalized(ModelsV5Base, ReprHelper):
|
|
||||||
__tablename__ = "difficulties_localized"
|
|
||||||
|
|
||||||
pkid: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
song_id: Mapped[str] = mapped_column(
|
|
||||||
ForeignKey(Difficulty.song_id, onupdate="CASCADE", ondelete="NO ACTION")
|
|
||||||
)
|
|
||||||
rating_class: Mapped[ArcaeaRatingClass] = mapped_column(
|
|
||||||
ForeignKey(Difficulty.rating_class, onupdate="CASCADE", ondelete="NO ACTION")
|
|
||||||
)
|
|
||||||
lang: Mapped[ArcaeaLanguage] = mapped_column(_ArcaeaLanguageEnumType)
|
|
||||||
title: Mapped[Optional[str]]
|
|
||||||
artist: Mapped[Optional[str]]
|
|
||||||
|
|
||||||
parent: Mapped[Difficulty] = relationship(
|
|
||||||
primaryjoin=and_(
|
|
||||||
Difficulty.song_id == song_id, Difficulty.rating_class == rating_class
|
|
||||||
),
|
|
||||||
viewonly=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ChartInfo(ModelsV5Base, ReprHelper):
|
|
||||||
__tablename__ = "charts_info"
|
|
||||||
|
|
||||||
song_id: Mapped[str] = mapped_column(
|
|
||||||
ForeignKey(Difficulty.song_id, onupdate="CASCADE", ondelete="NO ACTION"),
|
|
||||||
primary_key=True,
|
|
||||||
)
|
|
||||||
rating_class: Mapped[ArcaeaRatingClass] = mapped_column(
|
|
||||||
ForeignKey(Difficulty.rating_class, onupdate="CASCADE", ondelete="NO ACTION"),
|
|
||||||
primary_key=True,
|
|
||||||
)
|
|
||||||
constant: Mapped[int] = mapped_column(
|
|
||||||
comment="real_constant * 10. For example, Crimson Throne [FTR] is 10.4, then store 104."
|
|
||||||
)
|
|
||||||
notes: Mapped[Optional[int]]
|
|
||||||
|
|
||||||
difficulty: Mapped[Difficulty] = relationship(
|
|
||||||
primaryjoin=and_(
|
|
||||||
Difficulty.song_id == song_id, Difficulty.rating_class == rating_class
|
|
||||||
),
|
|
||||||
viewonly=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Chart(ModelsV5ViewBase, ReprHelper):
|
|
||||||
__tablename__ = "charts"
|
|
||||||
|
|
||||||
song_idx: Mapped[int]
|
|
||||||
song_id: Mapped[str]
|
|
||||||
rating_class: Mapped[ArcaeaRatingClass]
|
|
||||||
rating: Mapped[int]
|
|
||||||
rating_plus: Mapped[bool]
|
|
||||||
title: Mapped[str]
|
|
||||||
artist: Mapped[str]
|
|
||||||
pack_id: Mapped[str]
|
|
||||||
bpm: Mapped[Optional[str]]
|
|
||||||
bpm_base: Mapped[Optional[float]]
|
|
||||||
audio_preview: Mapped[Optional[int]]
|
|
||||||
audio_preview_end: Mapped[Optional[int]]
|
|
||||||
side: Mapped[Optional[int]]
|
|
||||||
version: Mapped[Optional[str]]
|
|
||||||
date: Mapped[Optional[datetime]]
|
|
||||||
bg: Mapped[Optional[str]]
|
|
||||||
bg_inverse: Mapped[Optional[str]]
|
|
||||||
bg_day: Mapped[Optional[str]]
|
|
||||||
bg_night: Mapped[Optional[str]]
|
|
||||||
source: Mapped[Optional[str]]
|
|
||||||
source_copyright: Mapped[Optional[str]]
|
|
||||||
chart_designer: Mapped[Optional[str]]
|
|
||||||
jacket_desginer: Mapped[Optional[str]]
|
|
||||||
audio_override: Mapped[bool]
|
|
||||||
jacket_override: Mapped[bool]
|
|
||||||
jacket_night: Mapped[Optional[str]]
|
|
||||||
constant: Mapped[int]
|
|
||||||
notes: Mapped[Optional[int]]
|
|
||||||
|
|
||||||
__table__ = create_view(
|
|
||||||
name=__tablename__,
|
|
||||||
selectable=select(
|
|
||||||
Song.idx.label("song_idx"),
|
|
||||||
Difficulty.song_id,
|
|
||||||
Difficulty.rating_class,
|
|
||||||
Difficulty.rating,
|
|
||||||
Difficulty.rating_plus,
|
|
||||||
func.coalesce(Difficulty.title, Song.title).label("title"),
|
|
||||||
func.coalesce(Difficulty.artist, Song.artist).label("artist"),
|
|
||||||
Song.pack_id,
|
|
||||||
func.coalesce(Difficulty.bpm, Song.bpm).label("bpm"),
|
|
||||||
func.coalesce(Difficulty.bpm_base, Song.bpm_base).label("bpm_base"),
|
|
||||||
Song.audio_preview,
|
|
||||||
Song.audio_preview_end,
|
|
||||||
Song.side,
|
|
||||||
func.coalesce(Difficulty.version, Song.version).label("version"),
|
|
||||||
func.coalesce(Difficulty.date, Song.date).label("date"),
|
|
||||||
func.coalesce(Difficulty.bg, Song.bg).label("bg"),
|
|
||||||
func.coalesce(Difficulty.bg_inverse, Song.bg_inverse).label("bg_inverse"),
|
|
||||||
Song.bg_day,
|
|
||||||
Song.bg_night,
|
|
||||||
Song.source,
|
|
||||||
Song.source_copyright,
|
|
||||||
Difficulty.chart_designer,
|
|
||||||
Difficulty.jacket_desginer,
|
|
||||||
Difficulty.audio_override,
|
|
||||||
Difficulty.jacket_override,
|
|
||||||
Difficulty.jacket_night,
|
|
||||||
ChartInfo.constant,
|
|
||||||
ChartInfo.notes,
|
|
||||||
)
|
|
||||||
.select_from(Difficulty)
|
|
||||||
.join(
|
|
||||||
ChartInfo,
|
|
||||||
(Difficulty.song_id == ChartInfo.song_id)
|
|
||||||
& (Difficulty.rating_class == ChartInfo.rating_class),
|
|
||||||
)
|
|
||||||
.join(Song, Difficulty.song_id == Song.id),
|
|
||||||
metadata=ModelsV5ViewBase.metadata,
|
|
||||||
cascade_on_drop=False,
|
|
||||||
)
|
|
85
src/arcaea_offline/database/models/chart.py
Normal file
85
src/arcaea_offline/database/models/chart.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
from sqlalchemy.orm import Mapped
|
||||||
|
from sqlalchemy_utils import create_view
|
||||||
|
|
||||||
|
from ._base import ModelBase, ModelViewBase, ReprHelper
|
||||||
|
from .chart_info import ChartInfo
|
||||||
|
from .difficulty import Difficulty
|
||||||
|
from .song import Song
|
||||||
|
|
||||||
|
|
||||||
|
class Chart(ModelBase, ReprHelper):
|
||||||
|
__tablename__ = "charts"
|
||||||
|
|
||||||
|
song_idx: Mapped[int]
|
||||||
|
song_id: Mapped[str]
|
||||||
|
rating_class: Mapped[int]
|
||||||
|
rating: Mapped[int]
|
||||||
|
is_rating_plus: Mapped[bool]
|
||||||
|
title: Mapped[str]
|
||||||
|
artist: Mapped[str]
|
||||||
|
pack_id: Mapped[str]
|
||||||
|
bpm: Mapped[Optional[str]]
|
||||||
|
bpm_base: Mapped[Optional[float]]
|
||||||
|
audio_preview: Mapped[Optional[int]]
|
||||||
|
audio_preview_end: Mapped[Optional[int]]
|
||||||
|
side: Mapped[Optional[int]]
|
||||||
|
version: Mapped[Optional[str]]
|
||||||
|
added_at: Mapped[Optional[datetime]]
|
||||||
|
bg: Mapped[Optional[str]]
|
||||||
|
bg_inverse: Mapped[Optional[str]]
|
||||||
|
bg_day: Mapped[Optional[str]]
|
||||||
|
bg_night: Mapped[Optional[str]]
|
||||||
|
source: Mapped[Optional[str]]
|
||||||
|
source_copyright: Mapped[Optional[str]]
|
||||||
|
chart_designer: Mapped[Optional[str]]
|
||||||
|
jacket_desginer: Mapped[Optional[str]]
|
||||||
|
has_overriding_audio: Mapped[bool]
|
||||||
|
has_overriding_jacket: Mapped[bool]
|
||||||
|
jacket_night: Mapped[Optional[str]]
|
||||||
|
constant: Mapped[int]
|
||||||
|
notes: Mapped[Optional[int]]
|
||||||
|
|
||||||
|
__table__ = create_view(
|
||||||
|
name=__tablename__,
|
||||||
|
selectable=select(
|
||||||
|
Song.idx.label("song_idx"),
|
||||||
|
Difficulty.song_id,
|
||||||
|
Difficulty.rating_class,
|
||||||
|
Difficulty.rating,
|
||||||
|
Difficulty.is_rating_plus,
|
||||||
|
func.coalesce(Difficulty.title, Song.title).label("title"),
|
||||||
|
func.coalesce(Difficulty.artist, Song.artist).label("artist"),
|
||||||
|
Song.pack_id,
|
||||||
|
func.coalesce(Difficulty.bpm, Song.bpm).label("bpm"),
|
||||||
|
func.coalesce(Difficulty.bpm_base, Song.bpm_base).label("bpm_base"),
|
||||||
|
Song.side,
|
||||||
|
func.coalesce(Difficulty.version, Song.version).label("version"),
|
||||||
|
func.coalesce(Difficulty.added_at, Song.added_at).label("added_at"),
|
||||||
|
func.coalesce(Difficulty.bg, Song.bg).label("bg"),
|
||||||
|
func.coalesce(Difficulty.bg_inverse, Song.bg_inverse).label("bg_inverse"),
|
||||||
|
Song.bg_day,
|
||||||
|
Song.bg_night,
|
||||||
|
Song.source,
|
||||||
|
Song.source_copyright,
|
||||||
|
Difficulty.chart_designer,
|
||||||
|
Difficulty.jacket_designer,
|
||||||
|
Difficulty.has_overriding_audio,
|
||||||
|
Difficulty.has_overriding_jacket,
|
||||||
|
Difficulty.jacket_night,
|
||||||
|
ChartInfo.constant,
|
||||||
|
ChartInfo.notes,
|
||||||
|
)
|
||||||
|
.select_from(Difficulty)
|
||||||
|
.join(
|
||||||
|
ChartInfo,
|
||||||
|
(Difficulty.song_id == ChartInfo.song_id)
|
||||||
|
& (Difficulty.rating_class == ChartInfo.rating_class),
|
||||||
|
)
|
||||||
|
.join(Song, Difficulty.song_id == Song.id),
|
||||||
|
metadata=ModelViewBase.metadata,
|
||||||
|
cascade_on_drop=False,
|
||||||
|
)
|
33
src/arcaea_offline/database/models/chart_info.py
Normal file
33
src/arcaea_offline/database/models/chart_info.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import ForeignKeyConstraint, Integer, Numeric, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from ._base import ModelBase, ReprHelper
|
||||||
|
from ._types import ForceTimezoneDateTime
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .difficulty import Difficulty
|
||||||
|
|
||||||
|
|
||||||
|
class ChartInfo(ModelBase, ReprHelper):
|
||||||
|
__tablename__ = "chart_info"
|
||||||
|
__table_args__ = (
|
||||||
|
ForeignKeyConstraint(
|
||||||
|
["song_id", "rating_class"],
|
||||||
|
["difficulty.song_id", "difficulty.rating_class"],
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
difficulty: Mapped["Difficulty"] = relationship(back_populates="chart_info_list")
|
||||||
|
|
||||||
|
song_id: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
|
rating_class: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
constant: Mapped[Decimal] = mapped_column(Numeric, nullable=False)
|
||||||
|
notes: Mapped[int] = mapped_column(Integer)
|
||||||
|
added_at: Mapped[datetime] = mapped_column(ForceTimezoneDateTime, primary_key=True)
|
||||||
|
version: Mapped[Optional[str]] = mapped_column(String)
|
@ -1,12 +1,12 @@
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from .base import ModelsV5Base, ReprHelper
|
from ._base import ModelBase, ReprHelper
|
||||||
|
|
||||||
__all__ = ["Property"]
|
__all__ = ["Property"]
|
||||||
|
|
||||||
|
|
||||||
class Property(ModelsV5Base, ReprHelper):
|
class Property(ModelBase, ReprHelper):
|
||||||
__tablename__ = "properties"
|
__tablename__ = "property"
|
||||||
|
|
||||||
key: Mapped[str] = mapped_column(primary_key=True)
|
key: Mapped[str] = mapped_column(primary_key=True)
|
||||||
value: Mapped[str] = mapped_column()
|
value: Mapped[str] = mapped_column()
|
||||||
|
92
src/arcaea_offline/database/models/difficulty.py
Normal file
92
src/arcaea_offline/database/models/difficulty.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
Boolean,
|
||||||
|
ForeignKey,
|
||||||
|
ForeignKeyConstraint,
|
||||||
|
Integer,
|
||||||
|
Numeric,
|
||||||
|
String,
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from ._base import ModelBase, ReprHelper
|
||||||
|
from ._types import ForceTimezoneDateTime
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .chart_info import ChartInfo
|
||||||
|
from .song import Song
|
||||||
|
|
||||||
|
|
||||||
|
class Difficulty(ModelBase, ReprHelper):
|
||||||
|
__tablename__ = "difficulty"
|
||||||
|
|
||||||
|
song_id: Mapped[str] = mapped_column(
|
||||||
|
ForeignKey("song.id", onupdate="CASCADE", ondelete="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
song: Mapped["Song"] = relationship(back_populates="difficulties")
|
||||||
|
localization_entries: Mapped[list["DifficultyLocalization"]] = relationship(
|
||||||
|
back_populates="difficulty",
|
||||||
|
cascade="all, delete",
|
||||||
|
passive_deletes=True,
|
||||||
|
)
|
||||||
|
chart_info_list: Mapped[list["ChartInfo"]] = relationship(
|
||||||
|
back_populates="difficulty",
|
||||||
|
cascade="all, delete",
|
||||||
|
passive_deletes=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
rating_class: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
|
||||||
|
rating: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
is_rating_plus: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, insert_default=False, server_default=text("0")
|
||||||
|
)
|
||||||
|
|
||||||
|
chart_designer: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
jacket_designer: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
|
||||||
|
has_overriding_audio: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, insert_default=False, server_default=text("0")
|
||||||
|
)
|
||||||
|
has_overriding_jacket: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, insert_default=False, server_default=text("0")
|
||||||
|
)
|
||||||
|
jacket_night: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
|
||||||
|
title: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
artist: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
bg: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
bg_inverse: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
bpm: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
bpm_base: Mapped[Optional[Decimal]] = mapped_column(Numeric(asdecimal=True))
|
||||||
|
added_at: Mapped[Optional[datetime]] = mapped_column(ForceTimezoneDateTime)
|
||||||
|
version: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
is_legacy11: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, insert_default=False, server_default=text("0")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DifficultyLocalization(ModelBase, ReprHelper):
|
||||||
|
__tablename__ = "difficulty_localization"
|
||||||
|
__table_args__ = (
|
||||||
|
ForeignKeyConstraint(
|
||||||
|
["song_id", "rating_class"],
|
||||||
|
["difficulty.song_id", "difficulty.rating_class"],
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
difficulty: Mapped["Difficulty"] = relationship(
|
||||||
|
back_populates="localization_entries"
|
||||||
|
)
|
||||||
|
song_id: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
|
rating_class: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
|
||||||
|
lang: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
|
title: Mapped[Optional[str]] = mapped_column(String)
|
61
src/arcaea_offline/database/models/pack.py
Normal file
61
src/arcaea_offline/database/models/pack.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import Boolean, ForeignKey, Integer, String, Text, text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from ._base import ModelBase, ReprHelper
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .song import Song
|
||||||
|
|
||||||
|
|
||||||
|
class Pack(ModelBase, ReprHelper):
|
||||||
|
__tablename__ = "pack"
|
||||||
|
|
||||||
|
songs: Mapped[list["Song"]] = relationship(
|
||||||
|
back_populates="pack",
|
||||||
|
cascade="all, delete",
|
||||||
|
passive_deletes=True,
|
||||||
|
)
|
||||||
|
localized_entries: Mapped[list["PackLocalization"]] = relationship(
|
||||||
|
back_populates="pack",
|
||||||
|
cascade="all, delete",
|
||||||
|
passive_deletes=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
|
name: Mapped[Optional[str]] = mapped_column(String, index=True)
|
||||||
|
description: Mapped[Optional[str]] = mapped_column(Text)
|
||||||
|
section: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
is_world_extend: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, insert_default=False, server_default=text("0")
|
||||||
|
)
|
||||||
|
|
||||||
|
plus_character: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
|
||||||
|
append_parent_id: Mapped[Optional[str]] = mapped_column(
|
||||||
|
ForeignKey("pack.id", onupdate="CASCADE", ondelete="CASCADE")
|
||||||
|
)
|
||||||
|
|
||||||
|
parent: Mapped["Pack"] = relationship(
|
||||||
|
"Pack",
|
||||||
|
back_populates="appendages",
|
||||||
|
cascade="all, delete",
|
||||||
|
passive_deletes=True,
|
||||||
|
remote_side=[id],
|
||||||
|
)
|
||||||
|
appendages: Mapped[list["Pack"]] = relationship("Pack", back_populates="parent")
|
||||||
|
|
||||||
|
|
||||||
|
class PackLocalization(ModelBase, ReprHelper):
|
||||||
|
__tablename__ = "pack_localization"
|
||||||
|
|
||||||
|
pack: Mapped["Pack"] = relationship(back_populates="localized_entries")
|
||||||
|
id: Mapped[str] = mapped_column(
|
||||||
|
ForeignKey("pack.id", onupdate="CASCADE", ondelete="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
lang: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
|
name: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
description: Mapped[Optional[str]] = mapped_column(Text)
|
@ -1,18 +1,14 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, and_, case, func, inspect, select, text
|
from sqlalchemy import Integer, String, Text, Uuid, case, func, inspect, select, text
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
from sqlalchemy_utils import create_view
|
from sqlalchemy_utils import create_view
|
||||||
|
|
||||||
from arcaea_offline.constants.enums import (
|
from ._base import ModelBase, ModelViewBase, ReprHelper
|
||||||
ArcaeaPlayResultClearType,
|
from .chart_info import ChartInfo
|
||||||
ArcaeaPlayResultModifier,
|
from .difficulty import Difficulty
|
||||||
ArcaeaRatingClass,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .arcaea import ChartInfo, Difficulty
|
|
||||||
from .base import ModelsV5Base, ModelsV5ViewBase, ReprHelper
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CalculatedPotential",
|
"CalculatedPotential",
|
||||||
@ -22,59 +18,53 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PlayResult(ModelsV5Base, ReprHelper):
|
class PlayResult(ModelBase, ReprHelper):
|
||||||
__tablename__ = "play_results"
|
__tablename__ = "play_result"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(autoincrement=True, primary_key=True)
|
id: Mapped[int] = mapped_column(autoincrement=True, primary_key=True)
|
||||||
song_id: Mapped[str] = mapped_column(
|
uuid: Mapped[UUID] = mapped_column(
|
||||||
ForeignKey(Difficulty.song_id, onupdate="CASCADE", ondelete="NO ACTION"),
|
Uuid, nullable=False, unique=True, default=lambda: uuid4()
|
||||||
index=True,
|
|
||||||
)
|
)
|
||||||
rating_class: Mapped[ArcaeaRatingClass] = mapped_column(
|
song_id: Mapped[str] = mapped_column(String)
|
||||||
ForeignKey(Difficulty.rating_class, onupdate="CASCADE", ondelete="NO ACTION"),
|
rating_class: Mapped[int] = mapped_column(Integer)
|
||||||
index=True,
|
played_at: Mapped[Optional[datetime]] = mapped_column(
|
||||||
|
default=lambda: datetime.now(timezone.utc)
|
||||||
)
|
)
|
||||||
score: Mapped[int]
|
score: Mapped[int]
|
||||||
pure: Mapped[Optional[int]]
|
pure: Mapped[Optional[int]]
|
||||||
|
pure_early: Mapped[Optional[int]]
|
||||||
|
pure_late: Mapped[Optional[int]]
|
||||||
far: Mapped[Optional[int]]
|
far: Mapped[Optional[int]]
|
||||||
|
far_early: Mapped[Optional[int]]
|
||||||
|
far_late: Mapped[Optional[int]]
|
||||||
lost: Mapped[Optional[int]]
|
lost: Mapped[Optional[int]]
|
||||||
date: Mapped[Optional[datetime]] = mapped_column(
|
|
||||||
default=lambda: datetime.now(timezone.utc)
|
|
||||||
)
|
|
||||||
max_recall: Mapped[Optional[int]]
|
max_recall: Mapped[Optional[int]]
|
||||||
modifier: Mapped[Optional[ArcaeaPlayResultModifier]]
|
clear_type: Mapped[Optional[int]]
|
||||||
clear_type: Mapped[Optional[ArcaeaPlayResultClearType]]
|
modifier: Mapped[Optional[int]]
|
||||||
comment: Mapped[Optional[str]]
|
comment: Mapped[Optional[str]] = mapped_column(Text)
|
||||||
|
|
||||||
difficulty: Mapped[Difficulty] = relationship(
|
|
||||||
primaryjoin=and_(
|
|
||||||
song_id == Difficulty.song_id,
|
|
||||||
rating_class == Difficulty.rating_class,
|
|
||||||
),
|
|
||||||
viewonly=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# How to create an SQL View with SQLAlchemy?
|
class PlayResultCalculated(ModelViewBase, ReprHelper):
|
||||||
# https://stackoverflow.com/a/53253105/16484891
|
|
||||||
# CC BY-SA 4.0
|
|
||||||
|
|
||||||
|
|
||||||
class PlayResultCalculated(ModelsV5ViewBase, ReprHelper):
|
|
||||||
__tablename__ = "play_results_calculated"
|
__tablename__ = "play_results_calculated"
|
||||||
|
|
||||||
id: Mapped[int]
|
id: Mapped[int]
|
||||||
|
uuid: Mapped[UUID]
|
||||||
song_id: Mapped[str]
|
song_id: Mapped[str]
|
||||||
rating_class: Mapped[ArcaeaRatingClass]
|
rating_class: Mapped[int]
|
||||||
score: Mapped[int]
|
score: Mapped[int]
|
||||||
pure: Mapped[Optional[int]]
|
pure: Mapped[Optional[int]]
|
||||||
|
pure_early: Mapped[Optional[int]]
|
||||||
|
pure_late: Mapped[Optional[int]]
|
||||||
shiny_pure: Mapped[Optional[int]]
|
shiny_pure: Mapped[Optional[int]]
|
||||||
far: Mapped[Optional[int]]
|
far: Mapped[Optional[int]]
|
||||||
|
far_early: Mapped[Optional[int]]
|
||||||
|
far_late: Mapped[Optional[int]]
|
||||||
lost: Mapped[Optional[int]]
|
lost: Mapped[Optional[int]]
|
||||||
date: Mapped[Optional[datetime]]
|
played_at: Mapped[Optional[datetime]]
|
||||||
max_recall: Mapped[Optional[int]]
|
max_recall: Mapped[Optional[int]]
|
||||||
modifier: Mapped[Optional[ArcaeaPlayResultModifier]]
|
modifier: Mapped[Optional[int]]
|
||||||
clear_type: Mapped[Optional[ArcaeaPlayResultClearType]]
|
clear_type: Mapped[Optional[int]]
|
||||||
potential: Mapped[float]
|
potential: Mapped[float]
|
||||||
comment: Mapped[Optional[str]]
|
comment: Mapped[Optional[str]]
|
||||||
|
|
||||||
@ -106,7 +96,7 @@ class PlayResultCalculated(ModelsV5ViewBase, ReprHelper):
|
|||||||
).label("shiny_pure"),
|
).label("shiny_pure"),
|
||||||
PlayResult.far,
|
PlayResult.far,
|
||||||
PlayResult.lost,
|
PlayResult.lost,
|
||||||
PlayResult.date,
|
PlayResult.played_at,
|
||||||
PlayResult.max_recall,
|
PlayResult.max_recall,
|
||||||
PlayResult.modifier,
|
PlayResult.modifier,
|
||||||
PlayResult.clear_type,
|
PlayResult.clear_type,
|
||||||
@ -137,26 +127,31 @@ class PlayResultCalculated(ModelsV5ViewBase, ReprHelper):
|
|||||||
(Difficulty.song_id == PlayResult.song_id)
|
(Difficulty.song_id == PlayResult.song_id)
|
||||||
& (Difficulty.rating_class == PlayResult.rating_class),
|
& (Difficulty.rating_class == PlayResult.rating_class),
|
||||||
),
|
),
|
||||||
metadata=ModelsV5ViewBase.metadata,
|
metadata=ModelViewBase.metadata,
|
||||||
cascade_on_drop=False,
|
cascade_on_drop=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PlayResultBest(ModelsV5ViewBase, ReprHelper):
|
class PlayResultBest(ModelViewBase, ReprHelper):
|
||||||
__tablename__ = "play_results_best"
|
__tablename__ = "play_results_best"
|
||||||
|
|
||||||
id: Mapped[int]
|
id: Mapped[int]
|
||||||
|
uuid: Mapped[UUID]
|
||||||
song_id: Mapped[str]
|
song_id: Mapped[str]
|
||||||
rating_class: Mapped[ArcaeaRatingClass]
|
rating_class: Mapped[int]
|
||||||
score: Mapped[int]
|
score: Mapped[int]
|
||||||
pure: Mapped[Optional[int]]
|
pure: Mapped[Optional[int]]
|
||||||
|
pure_early: Mapped[Optional[int]]
|
||||||
|
pure_late: Mapped[Optional[int]]
|
||||||
shiny_pure: Mapped[Optional[int]]
|
shiny_pure: Mapped[Optional[int]]
|
||||||
far: Mapped[Optional[int]]
|
far: Mapped[Optional[int]]
|
||||||
|
far_early: Mapped[Optional[int]]
|
||||||
|
far_late: Mapped[Optional[int]]
|
||||||
lost: Mapped[Optional[int]]
|
lost: Mapped[Optional[int]]
|
||||||
date: Mapped[Optional[datetime]]
|
played_at: Mapped[Optional[datetime]]
|
||||||
max_recall: Mapped[Optional[int]]
|
max_recall: Mapped[Optional[int]]
|
||||||
modifier: Mapped[Optional[ArcaeaPlayResultModifier]]
|
modifier: Mapped[Optional[int]]
|
||||||
clear_type: Mapped[Optional[ArcaeaPlayResultClearType]]
|
clear_type: Mapped[Optional[int]]
|
||||||
potential: Mapped[float]
|
potential: Mapped[float]
|
||||||
comment: Mapped[Optional[str]]
|
comment: Mapped[Optional[str]]
|
||||||
|
|
||||||
@ -173,12 +168,12 @@ class PlayResultBest(ModelsV5ViewBase, ReprHelper):
|
|||||||
.select_from(PlayResultCalculated)
|
.select_from(PlayResultCalculated)
|
||||||
.group_by(PlayResultCalculated.song_id, PlayResultCalculated.rating_class)
|
.group_by(PlayResultCalculated.song_id, PlayResultCalculated.rating_class)
|
||||||
.order_by(PlayResultCalculated.potential.desc()),
|
.order_by(PlayResultCalculated.potential.desc()),
|
||||||
metadata=ModelsV5ViewBase.metadata,
|
metadata=ModelViewBase.metadata,
|
||||||
cascade_on_drop=False,
|
cascade_on_drop=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CalculatedPotential(ModelsV5ViewBase, ReprHelper):
|
class CalculatedPotential(ModelViewBase, ReprHelper):
|
||||||
__tablename__ = "calculated_potential"
|
__tablename__ = "calculated_potential"
|
||||||
|
|
||||||
b30: Mapped[float]
|
b30: Mapped[float]
|
||||||
@ -192,6 +187,6 @@ class CalculatedPotential(ModelsV5ViewBase, ReprHelper):
|
|||||||
__table__ = create_view(
|
__table__ = create_view(
|
||||||
name=__tablename__,
|
name=__tablename__,
|
||||||
selectable=select(func.avg(_select_bests_subquery.c.b30_sum).label("b30")),
|
selectable=select(func.avg(_select_bests_subquery.c.b30_sum).label("b30")),
|
||||||
metadata=ModelsV5ViewBase.metadata,
|
metadata=ModelViewBase.metadata,
|
||||||
cascade_on_drop=False,
|
cascade_on_drop=False,
|
||||||
)
|
)
|
85
src/arcaea_offline/database/models/song.py
Normal file
85
src/arcaea_offline/database/models/song.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import Boolean, ForeignKey, Integer, Numeric, String, text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from ._base import ModelBase, ReprHelper
|
||||||
|
from ._types import ForceTimezoneDateTime
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .difficulty import Difficulty
|
||||||
|
from .pack import Pack
|
||||||
|
|
||||||
|
|
||||||
|
class Song(ModelBase, ReprHelper):
|
||||||
|
__tablename__ = "song"
|
||||||
|
|
||||||
|
pack_id: Mapped[str] = mapped_column(
|
||||||
|
ForeignKey("pack.id", onupdate="CASCADE", ondelete="CASCADE")
|
||||||
|
)
|
||||||
|
pack: Mapped["Pack"] = relationship(back_populates="songs")
|
||||||
|
difficulties: Mapped[list["Difficulty"]] = relationship(
|
||||||
|
back_populates="song",
|
||||||
|
cascade="all, delete",
|
||||||
|
passive_deletes=True,
|
||||||
|
)
|
||||||
|
localized_entries: Mapped[list["SongLocalization"]] = relationship(
|
||||||
|
back_populates="song",
|
||||||
|
cascade="all, delete",
|
||||||
|
passive_deletes=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
|
idx: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
title: Mapped[Optional[str]] = mapped_column(String, index=True)
|
||||||
|
artist: Mapped[Optional[str]] = mapped_column(String, index=True)
|
||||||
|
is_deleted: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, insert_default=False, server_default=text("0")
|
||||||
|
)
|
||||||
|
|
||||||
|
added_at: Mapped[datetime] = mapped_column(
|
||||||
|
ForceTimezoneDateTime, nullable=False, index=True
|
||||||
|
)
|
||||||
|
version: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
|
||||||
|
bpm: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
bpm_base: Mapped[Optional[Decimal]] = mapped_column(Numeric(asdecimal=True))
|
||||||
|
is_remote: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, insert_default=False, server_default=text("0")
|
||||||
|
)
|
||||||
|
is_unlockable_in_world: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, insert_default=False, server_default=text("0")
|
||||||
|
)
|
||||||
|
is_beyond_unlock_state_local: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, insert_default=False, server_default=text("0")
|
||||||
|
)
|
||||||
|
purchase: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
category: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
|
||||||
|
side: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
bg: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
bg_inverse: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
bg_day: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
bg_night: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
|
||||||
|
source: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
source_copyright: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
|
||||||
|
|
||||||
|
class SongLocalization(ModelBase, ReprHelper):
|
||||||
|
__tablename__ = "song_localization"
|
||||||
|
|
||||||
|
song: Mapped["Song"] = relationship(back_populates="localized_entries")
|
||||||
|
id: Mapped[str] = mapped_column(
|
||||||
|
ForeignKey("song.id", onupdate="CASCADE", ondelete="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
lang: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
|
title: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
source: Mapped[Optional[str]] = mapped_column(String)
|
||||||
|
has_jacket: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, insert_default=False, server_default=text("0")
|
||||||
|
)
|
Reference in New Issue
Block a user