Compare commits

...

3 Commits

Author SHA1 Message Date
6fb24d4907
refactor: play result exporters 2024-09-28 18:52:18 +08:00
eab2a3e520
refactor: smartrte b30 csv exporter 2024-09-28 18:36:27 +08:00
caced6eaec
fix: ruff PIE790 2024-09-28 17:42:00 +08:00
9 changed files with 120 additions and 145 deletions

View File

@ -1,13 +1,11 @@
import logging
import math
from typing import Iterable, List, Optional, Type, Union
from typing import Iterable, Optional, Type, Union
from sqlalchemy import Engine, func, inspect, select
from sqlalchemy.orm import DeclarativeBase, InstrumentedAttribute, sessionmaker
from arcaea_offline.external.arcsong.arcsong_json import ArcSongJsonBuilder
from arcaea_offline.external.exports import exporters
from arcaea_offline.external.exports.types import ArcaeaOfflineDEFV2_Score, ScoreExport
from arcaea_offline.singleton import Singleton
from .models.v4.config import ConfigBase, Property
@ -407,20 +405,6 @@ class Database(metaclass=Singleton):
# endregion
# region export
def export_scores(self) -> List[ScoreExport]:
scores = self.get_scores()
return [exporters.score(score) for score in scores]
def export_scores_def_v2(self) -> ArcaeaOfflineDEFV2_Score:
scores = self.get_scores()
return {
"$schema": "https://arcaeaoffline.sevive.xyz/schemas/def/v2/score.schema.json",
"type": "score",
"version": 2,
"scores": [exporters.score_def_v2(score) for score in scores],
}
def generate_arcsong(self):
with self.sessionmaker() as session:
arcsong = ArcSongJsonBuilder(session).generate_arcsong_json()

View File

@ -1,22 +1,7 @@
from typing import List, Literal, Optional, TypedDict
class ScoreExport(TypedDict):
id: int
song_id: str
rating_class: int
score: int
pure: Optional[int]
far: Optional[int]
lost: Optional[int]
date: Optional[int]
max_recall: Optional[int]
modifier: Optional[int]
clear_type: Optional[int]
comment: Optional[str]
class ArcaeaOfflineDEFV2_ScoreItem(TypedDict, total=False):
class ArcaeaOfflineDEFv2PlayResultItem(TypedDict, total=False):
id: Optional[int]
songId: str
ratingClass: int
@ -32,14 +17,14 @@ class ArcaeaOfflineDEFV2_ScoreItem(TypedDict, total=False):
comment: Optional[str]
ArcaeaOfflineDEFV2_Score = TypedDict(
"ArcaeaOfflineDEFV2_Score",
ArcaeaOfflineDEFv2PlayResultRoot = TypedDict(
"ArcaeaOfflineDEFv2PlayResultRoot",
{
"$schema": Literal[
"https://arcaeaoffline.sevive.xyz/schemas/def/v2/score.schema.json"
],
"type": Literal["score"],
"version": Literal[2],
"scores": List[ArcaeaOfflineDEFV2_ScoreItem],
"scores": List[ArcaeaOfflineDEFv2PlayResultItem],
},
)

View File

@ -0,0 +1,40 @@
from typing import List
from arcaea_offline.database.models.v5 import PlayResult
from .definitions import (
ArcaeaOfflineDEFv2PlayResultItem,
ArcaeaOfflineDEFv2PlayResultRoot,
)
class ArcaeaOfflineDEFv2PlayResultExporter:
def export(self, items: List[PlayResult]) -> ArcaeaOfflineDEFv2PlayResultRoot:
export_items = []
for item in items:
export_item: ArcaeaOfflineDEFv2PlayResultItem = {}
export_item["id"] = item.id
export_item["songId"] = item.song_id
export_item["ratingClass"] = item.rating_class.value
export_item["score"] = item.score
export_item["pure"] = item.pure
export_item["far"] = item.far
export_item["lost"] = item.lost
export_item["date"] = item.date
export_item["maxRecall"] = item.max_recall
export_item["modifier"] = (
item.modifier.value if item.modifier is not None else None
)
export_item["clearType"] = (
item.clear_type.value if item.clear_type is not None else None
)
export_item["source"] = "https://arcaeaoffline.sevive.xyz/python"
export_item["comment"] = item.comment
return {
"$schema": "https://arcaeaoffline.sevive.xyz/schemas/def/v2/score.schema.json",
"type": "score",
"version": 2,
"scores": export_items,
}

View File

@ -0,0 +1,75 @@
from typing import List, Tuple
from sqlalchemy import func
from sqlalchemy.orm import Session
from arcaea_offline.constants.enums.arcaea import ArcaeaRatingClass
from arcaea_offline.database.models.v5 import (
ChartInfo,
Difficulty,
PlayResultBest,
Song,
)
from arcaea_offline.utils.formatters.rating_class import RatingClassFormatter
class SmartRteBest30CsvExporter:
CSV_ROWS = [
"SongName",
"SongId",
"Difficulty",
"Score",
"Perfect",
"Perfect+",
"Far",
"Lost",
"Constant",
"PlayRating",
]
@classmethod
def rows(cls, session: Session) -> List:
results: List[
Tuple[str, str, ArcaeaRatingClass, int, int, int, int, int, float]
] = (
session.query(
func.coalesce(Difficulty.title, Song.title),
PlayResultBest.song_id,
PlayResultBest.rating_class,
PlayResultBest.score,
PlayResultBest.pure,
PlayResultBest.shiny_pure,
PlayResultBest.far,
PlayResultBest.lost,
ChartInfo.constant,
PlayResultBest.potential,
)
.join(
ChartInfo,
(ChartInfo.song_id == PlayResultBest.song_id)
& (ChartInfo.rating_class == PlayResultBest.rating_class),
)
.join(Song, (Song.id == PlayResultBest.song_id))
.join(
Difficulty,
(Difficulty.song_id == PlayResultBest.song_id)
& (Difficulty.rating_class == PlayResultBest.rating_class),
)
.all()
)
csv_rows = []
csv_rows.append(cls.CSV_ROWS.copy())
for _result in results:
result = list(_result)
# replace the comma in song title because the target project
# cannot handle quoted string
result[0] = result[0].replace(",", "") # type: ignore
result[2] = RatingClassFormatter.name(result[2]) # type: ignore
result[-2] = result[-2] / 10 # type: ignore
result[-1] = round(result[-1], 5) # type: ignore
csv_rows.append(result)
return csv_rows

View File

@ -1,38 +0,0 @@
from arcaea_offline.database.models.v4 import Score
from .types import ArcaeaOfflineDEFV2_ScoreItem, ScoreExport
def score(score: Score) -> ScoreExport:
return {
"id": score.id,
"song_id": score.song_id,
"rating_class": score.rating_class,
"score": score.score,
"pure": score.pure,
"far": score.far,
"lost": score.lost,
"date": score.date,
"max_recall": score.max_recall,
"modifier": score.modifier,
"clear_type": score.clear_type,
"comment": score.comment,
}
def score_def_v2(score: Score) -> ArcaeaOfflineDEFV2_ScoreItem:
return {
"id": score.id,
"songId": score.song_id,
"ratingClass": score.rating_class,
"score": score.score,
"pure": score.pure,
"far": score.far,
"lost": score.lost,
"date": score.date,
"maxRecall": score.max_recall,
"modifier": score.modifier,
"clearType": score.clear_type,
"source": None,
"comment": score.comment,
}

View File

@ -1,3 +0,0 @@
from .b30_csv import SmartRteB30CsvConverter
__all__ = ["SmartRteB30CsvConverter"]

View File

@ -1,64 +0,0 @@
from sqlalchemy.orm import Session
from ...models import Chart, ScoreBest
from ...utils.rating import rating_class_to_text
class SmartRteB30CsvConverter:
CSV_ROWS = [
"songname",
"songId",
"Difficulty",
"score",
"Perfect",
"criticalPerfect",
"Far",
"Lost",
"Constant",
"singlePTT",
]
def __init__(
self,
session: Session,
):
self.session = session
def rows(self) -> list:
csv_rows = [self.CSV_ROWS.copy()]
with self.session as session:
results = (
session.query(
Chart.title,
ScoreBest.song_id,
ScoreBest.rating_class,
ScoreBest.score,
ScoreBest.pure,
ScoreBest.shiny_pure,
ScoreBest.far,
ScoreBest.lost,
Chart.constant,
ScoreBest.potential,
)
.join(
Chart,
(Chart.song_id == ScoreBest.song_id)
& (Chart.rating_class == ScoreBest.rating_class),
)
.all()
)
for result in results:
# replace the comma in song title because the target project
# cannot handle quoted string
result = list(result)
result[0] = result[0].replace(",", "")
result[2] = rating_class_to_text(result[2])
# divide constant to float
result[-2] = result[-2] / 10
# round potential
result[-1] = round(result[-1], 5)
csv_rows.append(result)
return csv_rows

View File

@ -22,7 +22,6 @@ class RatingClassFormatter:
"""
Returns the capitalized rating class name, e.g. Future.
"""
...
@overload
@classmethod
@ -33,7 +32,6 @@ class RatingClassFormatter:
The integer will be converted to `ArcaeaRatingClass` enum,
and will return "Unknown" if the convertion fails.
"""
...
@classmethod
def name(cls, rating_class: Any) -> NAME_FORMAT_RESULTS:
@ -57,7 +55,6 @@ class RatingClassFormatter:
"""
Returns the uppercased rating class name, e.g. FTR.
"""
...
@overload
@classmethod
@ -68,7 +65,6 @@ class RatingClassFormatter:
The integer will be converted to `ArcaeaRatingClass` enum,
and will return "UNK" if the convertion fails.
"""
...
@classmethod
def abbreviation(cls, rating_class: Any) -> ABBREVIATION_FORMAT_RESULTS: