mirror of
https://github.com/283375/arcaea-offline.git
synced 2025-06-30 19:56:26 +00:00
Compare commits
8 Commits
62c85e9e82
...
v0.2.1
Author | SHA1 | Date | |
---|---|---|---|
0764308638
|
|||
e5c1e0ef4a
|
|||
8c48d76c65
|
|||
d79c73df8c
|
|||
7a64ec4a4a
|
|||
a9f8ba6e22
|
|||
13ea4d9e97
|
|||
24d46e4615
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "arcaea-offline"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
authors = [{ name = "283375", email = "log_283375@163.com" }]
|
||||
description = "Manage your local Arcaea score database."
|
||||
readme = "README.md"
|
||||
|
@ -84,11 +84,15 @@ class AndrealImageGeneratorApiDataConverter:
|
||||
raise ValueError("No score available.")
|
||||
best30_avg = self.session.scalar(select(CalculatedPotential.b30))
|
||||
|
||||
best30_overflow = (
|
||||
[self.score(score) for score in scores[30:40]] if len(scores) > 30 else []
|
||||
)
|
||||
|
||||
return {
|
||||
"content": {
|
||||
"account_info": self.account_info(),
|
||||
"best30_avg": best30_avg,
|
||||
"best30_list": [self.score(score) for score in scores[:30]],
|
||||
"best30_overflow": [self.score(score) for score in scores[-10:]],
|
||||
"best30_overflow": best30_overflow,
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
from .online import ArcaeaOnlineParser
|
||||
from .packlist import PacklistParser
|
||||
from .songlist import SonglistDifficultiesParser, SonglistParser
|
||||
from .st3 import St3ScoreParser
|
||||
|
30
src/arcaea_offline/external/arcaea/common.py
vendored
30
src/arcaea_offline/external/arcaea/common.py
vendored
@ -1,11 +1,41 @@
|
||||
import contextlib
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from os import PathLike
|
||||
from typing import Any, List, Optional, Union
|
||||
|
||||
from sqlalchemy.orm import DeclarativeBase, Session
|
||||
|
||||
|
||||
def fix_timestamp(timestamp: int) -> Union[int, None]:
|
||||
"""
|
||||
Some of the `date` column in st3 are strangely truncated. For example,
|
||||
a `1670283375` may be truncated to `167028`, even `1`. Yes, a single `1`.
|
||||
|
||||
To properly handle this situation, we check the timestamp's digits.
|
||||
If `digits < 5`, we treat this timestamp as a `None`. Otherwise, we try to
|
||||
fix the timestamp.
|
||||
|
||||
:param timestamp: a POSIX timestamp
|
||||
:return: `None` if the timestamp's digits < 5, otherwise a fixed POSIX timestamp
|
||||
"""
|
||||
# find digit length from https://stackoverflow.com/a/2189827/16484891
|
||||
# CC BY-SA 2.5
|
||||
# this might give incorrect result when timestamp > 999999999999997,
|
||||
# see https://stackoverflow.com/a/28883802/16484891 (CC BY-SA 4.0).
|
||||
# but that's way too later than 9999-12-31 23:59:59, 253402271999,
|
||||
# I don't think Arcaea would still be an active updated game by then.
|
||||
# so don't mind those small issues, just use this.
|
||||
digits = int(math.log10(abs(timestamp))) + 1 if timestamp != 0 else 1
|
||||
if digits < 5:
|
||||
return None
|
||||
timestamp_str = str(timestamp)
|
||||
current_timestamp_digits = int(math.log10(int(time.time()))) + 1
|
||||
timestamp_str = timestamp_str.ljust(current_timestamp_digits, "0")
|
||||
return int(timestamp_str, 10)
|
||||
|
||||
|
||||
def to_db_value(val: Any) -> Any:
|
||||
if not val:
|
||||
return None
|
||||
|
75
src/arcaea_offline/external/arcaea/online.py
vendored
Normal file
75
src/arcaea_offline/external/arcaea/online.py
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Literal, Optional, TypedDict
|
||||
|
||||
from ...models import Score
|
||||
from .common import ArcaeaParser, fix_timestamp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TWebApiRatingMeScoreItem(TypedDict):
|
||||
song_id: str
|
||||
difficulty: int
|
||||
modifier: int
|
||||
rating: float
|
||||
score: int
|
||||
perfect_count: int
|
||||
near_count: int
|
||||
miss_count: int
|
||||
clear_type: int
|
||||
title: Dict[Literal["ja", "en"], str]
|
||||
artist: str
|
||||
time_played: int
|
||||
bg: str
|
||||
|
||||
|
||||
class TWebApiRatingMeValue(TypedDict):
|
||||
best_rated_scores: List[TWebApiRatingMeScoreItem]
|
||||
recent_rated_scores: List[TWebApiRatingMeScoreItem]
|
||||
|
||||
|
||||
class TWebApiRatingMeResult(TypedDict):
|
||||
success: bool
|
||||
error_code: Optional[int]
|
||||
value: Optional[TWebApiRatingMeValue]
|
||||
|
||||
|
||||
class ArcaeaOnlineParser(ArcaeaParser):
|
||||
def __init__(self, filepath):
|
||||
super().__init__(filepath)
|
||||
|
||||
def parse(self) -> List[Score]:
|
||||
api_result_root: TWebApiRatingMeResult = json.loads(self.read_file_text())
|
||||
|
||||
api_result_value = api_result_root.get("value")
|
||||
if not api_result_value:
|
||||
error_code = api_result_root.get("error_code")
|
||||
raise ValueError(f"Cannot parse API result, error code {error_code}")
|
||||
|
||||
best30_score_items = api_result_value.get("best_rated_scores", [])
|
||||
recent_score_items = api_result_value.get("recent_rated_scores", [])
|
||||
score_items = best30_score_items + recent_score_items
|
||||
|
||||
date_text = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
results: List[Score] = []
|
||||
for score_item in score_items:
|
||||
score = Score()
|
||||
score.song_id = score_item["song_id"]
|
||||
score.rating_class = score_item["difficulty"]
|
||||
score.score = score_item["score"]
|
||||
score.pure = score_item["perfect_count"]
|
||||
score.far = score_item["near_count"]
|
||||
score.lost = score_item["miss_count"]
|
||||
score.date = fix_timestamp(int(score_item["time_played"] / 1000))
|
||||
score.modifier = score_item["modifier"]
|
||||
score.clear_type = score_item["clear_type"]
|
||||
|
||||
if score.lost == 0:
|
||||
score.max_recall = score.pure + score.far
|
||||
|
||||
score.comment = f"Parsed from web API at {date_text}"
|
||||
results.append(score)
|
||||
return results
|
9
src/arcaea_offline/external/arcaea/st3.py
vendored
9
src/arcaea_offline/external/arcaea/st3.py
vendored
@ -6,7 +6,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ...models.scores import Score
|
||||
from .common import ArcaeaParser
|
||||
from .common import ArcaeaParser, fix_timestamp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -37,9 +37,6 @@ class St3ScoreParser(ArcaeaParser):
|
||||
(song_id, rating_class),
|
||||
).fetchone()[0]
|
||||
|
||||
date_str = str(date)
|
||||
date = None if len(date_str) < 7 else int(date_str.ljust(10, "0"))
|
||||
|
||||
items.append(
|
||||
Score(
|
||||
song_id=song_id,
|
||||
@ -48,10 +45,10 @@ class St3ScoreParser(ArcaeaParser):
|
||||
pure=pure,
|
||||
far=far,
|
||||
lost=lost,
|
||||
date=date,
|
||||
date=fix_timestamp(date),
|
||||
modifier=modifier,
|
||||
clear_type=clear_type,
|
||||
comment="Imported from st3",
|
||||
comment=f"Parsed from st3",
|
||||
)
|
||||
)
|
||||
|
||||
|
1
src/arcaea_offline/external/smartrte/__init__.py
vendored
Normal file
1
src/arcaea_offline/external/smartrte/__init__.py
vendored
Normal file
@ -0,0 +1 @@
|
||||
from .b30_csv import SmartRteB30CsvConverter
|
64
src/arcaea_offline/external/smartrte/b30_csv.py
vendored
Normal file
64
src/arcaea_offline/external/smartrte/b30_csv.py
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
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
|
@ -80,7 +80,12 @@ class ScoreCalculated(ScoresViewBase):
|
||||
(
|
||||
case(
|
||||
(
|
||||
(ChartInfo.notes.isnot(None) & ChartInfo.notes != 0),
|
||||
(
|
||||
ChartInfo.notes.is_not(None)
|
||||
& Score.pure.is_not(None)
|
||||
& Score.far.is_not(None)
|
||||
& (ChartInfo.notes != 0)
|
||||
),
|
||||
Score.score
|
||||
- func.floor(
|
||||
(Score.pure * 10000000.0 / ChartInfo.notes)
|
||||
|
Reference in New Issue
Block a user