mirror of
https://github.com/283375/arcaea-offline.git
synced 2025-09-13 06:53:09 +00:00
Compare commits
8 Commits
2b8b13ca95
...
0.3.0-refa
Author | SHA1 | Date | |
---|---|---|---|
2dc96e06aa
|
|||
308b087b94
|
|||
a8164f37e2
|
|||
ab03b27730
|
|||
ff248d1363
|
|||
4c46b43d4a
|
|||
826e097d2e
|
|||
02788878a5
|
@ -1,5 +1,6 @@
|
||||
from .arcaea import (
|
||||
ArcaeaLanguage,
|
||||
ArcaeaLocalizationLanguage,
|
||||
ArcaeaPlayResultClearType,
|
||||
ArcaeaPlayResultModifier,
|
||||
ArcaeaRatingClass,
|
||||
@ -8,6 +9,7 @@ from .arcaea import (
|
||||
|
||||
__all__ = [
|
||||
"ArcaeaLanguage",
|
||||
"ArcaeaLocalizationLanguage",
|
||||
"ArcaeaPlayResultClearType",
|
||||
"ArcaeaPlayResultModifier",
|
||||
"ArcaeaRatingClass",
|
||||
|
@ -31,6 +31,13 @@ class ArcaeaPlayResultClearType(IntEnum):
|
||||
EASY_CLEAR = 5
|
||||
|
||||
|
||||
class ArcaeaLocalizationLanguage(Enum):
|
||||
JA = "ja"
|
||||
KO = "ko"
|
||||
ZH_HANT = "zh-Hant"
|
||||
ZH_HANS = "zh-Hans"
|
||||
|
||||
|
||||
class ArcaeaLanguage(Enum):
|
||||
EN = "en"
|
||||
JA = "ja"
|
||||
|
@ -11,7 +11,6 @@ from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
|
@ -15,7 +15,6 @@ from alembic import context, op
|
||||
|
||||
from arcaea_offline.database.migrations.legacies.v5 import ForceTimezoneDateTime
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "0ca6733e40dc"
|
||||
down_revision: Union[str, None] = "a3f9d48b7de3"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
@ -27,7 +26,7 @@ def upgrade(
|
||||
data_migration: bool = True,
|
||||
data_migration_options: Any = None,
|
||||
) -> None:
|
||||
op.create_table(
|
||||
property_tbl = op.create_table(
|
||||
"property",
|
||||
sa.Column("key", sa.String(), nullable=False),
|
||||
sa.Column("value", sa.String(), nullable=False),
|
||||
@ -295,6 +294,8 @@ def upgrade(
|
||||
|
||||
op.drop_table("scores_old")
|
||||
|
||||
op.execute(sa.insert(property_tbl).values(key="version", value="5"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
raise NotImplementedError(
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""v1 to v4
|
||||
|
||||
Revision ID: a3f9d48b7de3
|
||||
Revises:
|
||||
Revises: b7a0accfc17f
|
||||
Create Date: 2024-11-24 00:03:07.697165
|
||||
|
||||
"""
|
||||
@ -12,9 +12,8 @@ from typing import Mapping, Optional, Sequence, TypedDict, Union
|
||||
import sqlalchemy as sa
|
||||
from alembic import context, op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "a3f9d48b7de3"
|
||||
down_revision: Union[str, None] = None
|
||||
down_revision: Union[str, None] = "b7a0accfc17f"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
@ -268,6 +267,16 @@ def upgrade(
|
||||
|
||||
op.drop_table("scores_old")
|
||||
|
||||
op.drop_table("properties", if_exists=True)
|
||||
properties_tbl = op.create_table(
|
||||
"properties",
|
||||
sa.Column("key", sa.TEXT(), nullable=False),
|
||||
sa.Column("value", sa.TEXT(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("key", name="pk_properties"),
|
||||
)
|
||||
|
||||
op.execute(sa.insert(properties_tbl).values(key="version", value="4"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
raise NotImplementedError(
|
||||
|
@ -0,0 +1,106 @@
|
||||
"""v1
|
||||
|
||||
Revision ID: b7a0accfc17f
|
||||
Revises:
|
||||
Create Date: 2025-06-05 22:17:13.327742
|
||||
|
||||
"""
|
||||
|
||||
from typing import Any, Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "b7a0accfc17f"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade(
|
||||
*,
|
||||
data_migration: bool = False,
|
||||
data_migration_options: Any = None,
|
||||
) -> None:
|
||||
op.create_table(
|
||||
"charts",
|
||||
sa.Column("song_id", sa.TEXT(), nullable=False),
|
||||
sa.Column("rating_class", sa.INTEGER(), nullable=False),
|
||||
sa.Column("name_en", sa.TEXT(), nullable=False),
|
||||
sa.Column("name_jp", sa.TEXT()),
|
||||
sa.Column("artist", sa.TEXT(), nullable=False),
|
||||
sa.Column("bpm", sa.TEXT(), nullable=False),
|
||||
sa.Column("bpm_base", sa.REAL(), nullable=False),
|
||||
sa.Column("package_id", sa.TEXT(), nullable=False),
|
||||
sa.Column("time", sa.INTEGER()),
|
||||
sa.Column("side", sa.INTEGER(), nullable=False),
|
||||
sa.Column("world_unlock", sa.BOOLEAN(), nullable=False),
|
||||
sa.Column("remote_download", sa.BOOLEAN()),
|
||||
sa.Column("bg", sa.TEXT(), nullable=False),
|
||||
sa.Column("date", sa.INTEGER(), nullable=False),
|
||||
sa.Column("version", sa.TEXT(), nullable=False),
|
||||
sa.Column("difficulty", sa.INTEGER(), nullable=False),
|
||||
sa.Column("rating", sa.INTEGER(), nullable=False),
|
||||
sa.Column("note", sa.INTEGER(), nullable=False),
|
||||
sa.Column("chart_designer", sa.TEXT()),
|
||||
sa.Column("jacket_designer", sa.TEXT()),
|
||||
sa.Column("jacket_override", sa.BOOLEAN(), nullable=False),
|
||||
sa.Column("audio_override", sa.BOOLEAN(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("song_id", "rating_class", name="pk_charts"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"aliases",
|
||||
sa.Column("song_id", sa.TEXT(), nullable=False),
|
||||
sa.Column("alias", sa.TEXT(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("song_id", "alias", name="pk_aliases"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"packages",
|
||||
sa.Column("package_id", sa.TEXT(), nullable=False),
|
||||
sa.Column("name", sa.TEXT(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("package_id", name="pk_packages"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"scores",
|
||||
sa.Column("id", sa.INTEGER(), autoincrement=True),
|
||||
sa.Column("song_id", sa.TEXT(), nullable=False),
|
||||
sa.Column("rating_class", sa.INTEGER(), nullable=False),
|
||||
sa.Column("score", sa.INTEGER(), nullable=False),
|
||||
sa.Column("pure", sa.INTEGER()),
|
||||
sa.Column("far", sa.INTEGER()),
|
||||
sa.Column("lost", sa.INTEGER()),
|
||||
sa.Column("time", sa.INTEGER(), nullable=False),
|
||||
sa.Column("max_recall", sa.INTEGER()),
|
||||
sa.Column("clear_type", sa.INTEGER()),
|
||||
sa.PrimaryKeyConstraint("id", name="pk_scores"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["song_id", "rating_class"],
|
||||
["charts.song_id", "charts.rating_class"],
|
||||
name="fk_scores_song_id_charts",
|
||||
onupdate="CASCADE",
|
||||
ondelete="NO ACTION",
|
||||
),
|
||||
)
|
||||
|
||||
properties_tbl = op.create_table(
|
||||
"properties",
|
||||
sa.Column("key", sa.TEXT(), nullable=False),
|
||||
sa.Column("value", sa.TEXT(), nullable=False),
|
||||
# according to the commit history this was a unique constraint
|
||||
# but i remember sqlalchemy complains if you don't add a primary key
|
||||
# anyways above tables have modifications like this too
|
||||
# and nobody actualty uses v1 schema so who cares
|
||||
sa.PrimaryKeyConstraint("key", name="pk_properties"),
|
||||
)
|
||||
|
||||
# there should be CREATE VIEWs here but im skipping them
|
||||
# because nobody actually checks this file
|
||||
|
||||
op.execute(sa.insert(properties_tbl).values(key="db_version", value="1"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
raise NotImplementedError("This is the first revision bro")
|
@ -0,0 +1,31 @@
|
||||
"""version date
|
||||
|
||||
Revision ID: f82ac2f445a5
|
||||
Revises: 0ca6733e40dc
|
||||
Create Date: 2025-07-19 17:11:27.448574
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "f82ac2f445a5"
|
||||
down_revision: Union[str, None] = "0ca6733e40dc"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"version_date",
|
||||
sa.Column("version", sa.String(), nullable=False),
|
||||
sa.Column("songlist_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("published_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("version", name=op.f("pk_version_date")),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("version_date")
|
@ -4,6 +4,7 @@ from .config import Property
|
||||
from .difficulty import Difficulty, DifficultyLocalization
|
||||
from .pack import Pack, PackLocalization
|
||||
from .song import Song, SongLocalization
|
||||
from .version_date import VersionDate
|
||||
|
||||
from .chart import Chart # isort: skip
|
||||
from .play_result import (
|
||||
@ -29,4 +30,5 @@ __all__ = [
|
||||
"Property",
|
||||
"Song",
|
||||
"SongLocalization",
|
||||
"VersionDate",
|
||||
]
|
||||
|
@ -4,10 +4,13 @@ from sqlalchemy import MetaData
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy.orm.exc import DetachedInstanceError
|
||||
|
||||
from ._types import ForceTimezoneDateTime
|
||||
from arcaea_offline.utils import Version
|
||||
|
||||
from ._types import ForceTimezoneDateTime, VersionDatabaseType
|
||||
|
||||
TYPE_ANNOTATION_MAP = {
|
||||
datetime: ForceTimezoneDateTime,
|
||||
Version: VersionDatabaseType,
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import DateTime
|
||||
from sqlalchemy import DateTime, String
|
||||
from sqlalchemy.types import TypeDecorator
|
||||
|
||||
from arcaea_offline.utils import Version
|
||||
|
||||
|
||||
class ForceTimezoneDateTime(TypeDecorator):
|
||||
"""
|
||||
@ -26,3 +28,23 @@ class ForceTimezoneDateTime(TypeDecorator):
|
||||
if value is not None:
|
||||
value = value.replace(tzinfo=timezone.utc)
|
||||
return value
|
||||
|
||||
|
||||
class VersionDatabaseType(TypeDecorator):
|
||||
impl = String
|
||||
cache_ok = True
|
||||
|
||||
def process_bind_param(self, value: Optional[Version], dialect):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if not isinstance(value, Version):
|
||||
raise ValueError("Input is not a Version instance.")
|
||||
|
||||
return str(f"{value.first}.{value.second}.{value.third}")
|
||||
|
||||
def process_result_value(self, value: Optional[str], dialect):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return Version(*(map(int, value.split("."))))
|
||||
|
@ -5,6 +5,8 @@ from typing import TYPE_CHECKING, Optional
|
||||
from sqlalchemy import ForeignKeyConstraint, Integer, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from arcaea_offline.utils import Version
|
||||
|
||||
from ._base import ModelBase, ReprHelper
|
||||
from ._types import ForceTimezoneDateTime
|
||||
|
||||
@ -28,6 +30,6 @@ class ChartInfo(ModelBase, ReprHelper):
|
||||
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)
|
||||
notes: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
added_at: Mapped[datetime] = mapped_column(ForceTimezoneDateTime, primary_key=True)
|
||||
version: Mapped[Optional[str]] = mapped_column(String)
|
||||
version: Mapped[Optional[Version]]
|
||||
|
@ -13,6 +13,8 @@ from sqlalchemy import (
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from arcaea_offline.utils import Version
|
||||
|
||||
from ._base import ModelBase, ReprHelper
|
||||
from ._types import ForceTimezoneDateTime
|
||||
|
||||
@ -65,7 +67,7 @@ class Difficulty(ModelBase, ReprHelper):
|
||||
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)
|
||||
version: Mapped[Optional[Version]]
|
||||
is_legacy11: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, insert_default=False, server_default=text("0")
|
||||
)
|
||||
|
@ -5,6 +5,8 @@ from typing import TYPE_CHECKING, Optional
|
||||
from sqlalchemy import Boolean, ForeignKey, Integer, Numeric, String, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from arcaea_offline.utils import Version
|
||||
|
||||
from ._base import ModelBase, ReprHelper
|
||||
from ._types import ForceTimezoneDateTime
|
||||
|
||||
@ -42,7 +44,7 @@ class Song(ModelBase, ReprHelper):
|
||||
added_at: Mapped[datetime] = mapped_column(
|
||||
ForceTimezoneDateTime, nullable=False, index=True
|
||||
)
|
||||
version: Mapped[Optional[str]] = mapped_column(String)
|
||||
version: Mapped[Optional[Version]]
|
||||
|
||||
bpm: Mapped[Optional[str]] = mapped_column(String)
|
||||
bpm_base: Mapped[Optional[Decimal]] = mapped_column(Numeric(asdecimal=True))
|
||||
|
15
src/arcaea_offline/database/models/version_date.py
Normal file
15
src/arcaea_offline/database/models/version_date.py
Normal file
@ -0,0 +1,15 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from arcaea_offline.utils import Version
|
||||
|
||||
from ._base import ModelBase, ReprHelper
|
||||
|
||||
|
||||
class VersionDate(ModelBase, ReprHelper):
|
||||
__tablename__ = "version_date"
|
||||
|
||||
version: Mapped[Version] = mapped_column(primary_key=True)
|
||||
songlist_at: Mapped[datetime]
|
||||
published_at: Mapped[datetime]
|
@ -0,0 +1,5 @@
|
||||
from .version import Version
|
||||
|
||||
__all__ = [
|
||||
"Version",
|
||||
]
|
||||
|
27
src/arcaea_offline/utils/version.py
Normal file
27
src/arcaea_offline/utils/version.py
Normal file
@ -0,0 +1,27 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class Version(NamedTuple):
|
||||
first: int
|
||||
second: int
|
||||
third: int
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, version_str: str):
|
||||
version_str = version_str.removesuffix("c")
|
||||
parts = version_str.split(".")
|
||||
if len(parts) not in {2, 3}:
|
||||
raise ValueError(f"Invalid version string {version_str}")
|
||||
|
||||
try:
|
||||
if len(parts) == 2: # noqa: PLR2004
|
||||
parts.append("0")
|
||||
|
||||
first, second, third = map(int, parts)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid version string {version_str}") from e
|
||||
|
||||
return cls(first, second, third)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.first}.{self.second}.{self.third}"
|
Reference in New Issue
Block a user