262 lines
9.3 KiB
Python
262 lines
9.3 KiB
Python
"""
|
|
Copyright (C) 2025 283375
|
|
|
|
This file is part of "arcaea-apk-assets" (stated as "this program" below).
|
|
|
|
This program is free software: you can redistribute it and/or modify it under
|
|
the terms of the GNU General Public License as published by the Free Software
|
|
Foundation, either version 3 of the License, or (at your option) any later
|
|
version.
|
|
|
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along with
|
|
this program. If not, see <https://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
|
|
from arcaea_offline_schemas.dis.v5.dataclass import (
|
|
AodisDifficulty,
|
|
AodisDifficultyLocalization,
|
|
AodisSong,
|
|
AodisSongLocalization,
|
|
)
|
|
from arcaea_offline_schemas.dis.v5.dataclass.shared import (
|
|
AddedAtObject,
|
|
Backgrounds,
|
|
BpmObject,
|
|
)
|
|
from arcaea_offline_schemas.dis.v5.dataclass.song import Source
|
|
from typeguard import TypeCheckError, check_type
|
|
|
|
from ._shared import blank_str_to_none
|
|
from .defs import ARCAEA_LANGUAGE_KEYS
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SonglistFormatError(Exception):
|
|
pass
|
|
|
|
|
|
class ArcaeaSonglistParser:
|
|
@staticmethod
|
|
def check_root_content_format(root: dict) -> None:
|
|
try:
|
|
check_type(root, dict)
|
|
check_type(root["songs"], list)
|
|
except (KeyError, TypeCheckError) as e:
|
|
raise SonglistFormatError from e
|
|
|
|
@staticmethod
|
|
def check_song_object_format(song: dict) -> None:
|
|
try:
|
|
check_type(song, dict)
|
|
check_type(song["id"], str)
|
|
|
|
if deleted := song.get("deleted"):
|
|
check_type(deleted, bool)
|
|
|
|
bg_daynight = song.get("bg_daynight", {})
|
|
bg = blank_str_to_none(song.get("bg"))
|
|
bg_inverse = blank_str_to_none(song.get("bg_inverse"))
|
|
bg_day = blank_str_to_none(bg_daynight.get("day"))
|
|
bg_night = blank_str_to_none(bg_daynight.get("night"))
|
|
if (bg_inverse or bg_day or bg_night) and not bg:
|
|
msg = "One of bg_inverse, bg_day and bg_night is set without bg"
|
|
raise SonglistFormatError(msg)
|
|
|
|
except (KeyError, TypeCheckError) as e:
|
|
raise SonglistFormatError from e
|
|
|
|
# TODO(283375): localized objects & difficulties (strict list[dict]) & booleans # noqa: E501, FIX002, TD003
|
|
|
|
@staticmethod
|
|
def parse_song(song: dict) -> list[AodisSong | AodisSongLocalization]:
|
|
id_ = song["id"]
|
|
title_localized = song.get("title_localized", {})
|
|
bg_daynight = song.get("bg_daynight", {})
|
|
source_localized = song.get("source_localized", {})
|
|
jacket_localized = song.get("jacket_localized", {})
|
|
|
|
bg = blank_str_to_none(song.get("bg"))
|
|
bg_inverse = blank_str_to_none(song.get("bg_inverse"))
|
|
bg_day = blank_str_to_none(bg_daynight.get("day"))
|
|
bg_night = blank_str_to_none(bg_daynight.get("night"))
|
|
|
|
results = []
|
|
|
|
aodis_song = AodisSong(
|
|
pack_id=song["set"],
|
|
id=id_,
|
|
addedAt=AddedAtObject(
|
|
date=datetime.fromtimestamp(song["date"], tz=timezone.utc).isoformat(),
|
|
version=blank_str_to_none(song.get("version")),
|
|
),
|
|
idx=song.get("idx"),
|
|
title=blank_str_to_none(title_localized.get("en")),
|
|
artist=blank_str_to_none(song.get("artist")),
|
|
isDeleted=song.get("deleted", False),
|
|
bpm=BpmObject(
|
|
value=song["bpm_base"],
|
|
display=blank_str_to_none(song.get("bpm")),
|
|
)
|
|
if song.get("bpm_base") is not None
|
|
else None,
|
|
isRemote=song.get("remote_dl", False),
|
|
isUnlockableInWorld=song.get("world_unlock", False),
|
|
isBeyondUnlockStateLocal=song.get("byd_local_unlock", False),
|
|
purchase=blank_str_to_none(song.get("purchase")),
|
|
category=blank_str_to_none(song.get("category")),
|
|
side=song.get("side"),
|
|
background=Backgrounds(
|
|
base=bg or bg_inverse or bg_day or bg_night,
|
|
inverse=blank_str_to_none(song.get("bg_inverse")),
|
|
day=blank_str_to_none(bg_daynight.get("day")),
|
|
night=blank_str_to_none(bg_daynight.get("night")),
|
|
)
|
|
if bg is not None
|
|
else None,
|
|
source=Source(
|
|
display=source_localized["en"],
|
|
copyright=blank_str_to_none(song.get("source_copyright")),
|
|
)
|
|
if source_localized.get("en") is not None
|
|
else None,
|
|
)
|
|
results.append(aodis_song)
|
|
|
|
for lang_key in ARCAEA_LANGUAGE_KEYS:
|
|
title_l10n = blank_str_to_none(title_localized.get(lang_key))
|
|
source_l10n = blank_str_to_none(source_localized.get(lang_key))
|
|
has_localized_jacket = jacket_localized.get(lang_key)
|
|
|
|
if title_l10n or source_l10n or has_localized_jacket:
|
|
results.append(
|
|
AodisSongLocalization(
|
|
id=id_,
|
|
lang=lang_key,
|
|
title=title_l10n,
|
|
source=source_l10n,
|
|
hasJacket=has_localized_jacket,
|
|
),
|
|
)
|
|
|
|
return results
|
|
|
|
@staticmethod
|
|
def parse_difficulties(
|
|
song: dict,
|
|
) -> list[AodisDifficulty | AodisDifficultyLocalization]:
|
|
id_ = song["id"]
|
|
difficulties = song["difficulties"]
|
|
|
|
results = []
|
|
|
|
for difficulty in difficulties:
|
|
difficulty_title_localized = difficulty.get("title_localized", {})
|
|
difficulty_artist_localized = difficulty.get("artist_localized", {})
|
|
rating_class = difficulty["ratingClass"]
|
|
|
|
results.append(
|
|
AodisDifficulty(
|
|
songId=id_,
|
|
ratingClass=rating_class,
|
|
rating=difficulty["rating"],
|
|
isRatingPlus=difficulty.get("ratingPlus", False),
|
|
chartDesigner=blank_str_to_none(
|
|
difficulty.get("chartDesigner"),
|
|
),
|
|
jacketDesigner=blank_str_to_none(
|
|
difficulty.get("jacketDesigner"),
|
|
),
|
|
hasOverridingAudio=difficulty.get("audioOverride", False),
|
|
hasOverridingJacket=difficulty.get("jacketOverride", False),
|
|
jacketNight=blank_str_to_none(difficulty.get("jacket_night")),
|
|
addedAt=AddedAtObject(
|
|
date=datetime.fromtimestamp(
|
|
difficulty["date"],
|
|
tz=timezone.utc,
|
|
).isoformat(),
|
|
version=blank_str_to_none(difficulty.get("version")),
|
|
)
|
|
if difficulty.get("date")
|
|
else None,
|
|
background=Backgrounds(base=difficulty["bg"])
|
|
if difficulty.get("bg") is not None
|
|
else None,
|
|
title=blank_str_to_none(difficulty_title_localized.get("en")),
|
|
artist=blank_str_to_none(difficulty_artist_localized.get("en")),
|
|
isLegacy11=difficulty.get("legacy11", False),
|
|
),
|
|
)
|
|
|
|
for lang_key in ARCAEA_LANGUAGE_KEYS:
|
|
difficuly_title_l10n = blank_str_to_none(
|
|
difficulty_title_localized.get(lang_key),
|
|
)
|
|
difficuly_artist_l10n = blank_str_to_none(
|
|
difficulty_artist_localized.get(lang_key),
|
|
)
|
|
|
|
if difficuly_title_l10n:
|
|
results.append(
|
|
AodisDifficultyLocalization(
|
|
songId=id_,
|
|
ratingClass=rating_class,
|
|
lang=lang_key,
|
|
title=difficuly_title_l10n,
|
|
artist=difficuly_artist_l10n,
|
|
),
|
|
)
|
|
|
|
return results
|
|
|
|
@classmethod
|
|
def items(
|
|
cls,
|
|
songlist_content: dict,
|
|
*,
|
|
skip_format_checks: bool = False,
|
|
) -> list[
|
|
AodisSong
|
|
| AodisSongLocalization
|
|
| AodisDifficulty
|
|
| AodisDifficultyLocalization
|
|
]:
|
|
try:
|
|
cls.check_root_content_format(songlist_content)
|
|
except SonglistFormatError:
|
|
if skip_format_checks:
|
|
logger.exception("Error skipped during songlist parsing:")
|
|
return []
|
|
|
|
raise
|
|
|
|
songs = songlist_content["songs"]
|
|
results = []
|
|
|
|
for song in songs:
|
|
try:
|
|
cls.check_song_object_format(song)
|
|
except SonglistFormatError:
|
|
if skip_format_checks:
|
|
logger.exception("Error skipped during songlist parsing:")
|
|
continue
|
|
|
|
raise
|
|
|
|
if song.get("deleted"):
|
|
continue
|
|
|
|
results.extend(cls.parse_song(song))
|
|
results.extend(cls.parse_difficulties(song))
|
|
|
|
return results
|