209 lines
8.3 KiB
Python
209 lines
8.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/>.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
|
|
from ._shared import blank_str_to_none
|
|
from .defs import (
|
|
ARCAEA_LANGUAGE_KEYS,
|
|
Difficulty,
|
|
DifficultyLocalization,
|
|
Song,
|
|
SongLocalization,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SonglistFormatError(Exception):
|
|
pass
|
|
|
|
|
|
class ArcaeaSonglistParser:
|
|
@staticmethod
|
|
def _assert_root_content(root: Any):
|
|
if not isinstance(root, dict):
|
|
msg = f"songlist root should be `dict`, got {type(root)}"
|
|
raise SonglistFormatError(msg)
|
|
|
|
songs = root.get("songs")
|
|
if songs is None:
|
|
msg = "`songs` field does not exist"
|
|
raise SonglistFormatError(msg)
|
|
|
|
if not isinstance(songs, list):
|
|
msg = f"`songs` field should be `list`, got {type(songs)}"
|
|
raise SonglistFormatError(msg)
|
|
|
|
@staticmethod
|
|
def _assert_song_object(obj: Any):
|
|
if not isinstance(obj, dict):
|
|
msg = f"A song object should be `dict`, got {type(obj)}"
|
|
raise SonglistFormatError(msg)
|
|
|
|
id_ = obj.get("id")
|
|
if not isinstance(id_, str):
|
|
msg = f"`id` attribute of a song should be `str`, got value {id_!r}"
|
|
raise SonglistFormatError(msg)
|
|
|
|
is_deleted = obj.get("deleted", False)
|
|
if not isinstance(is_deleted, bool):
|
|
msg = f"`deleted` attribute of a song should be `bool`, got value {id_!r}"
|
|
raise SonglistFormatError(msg)
|
|
|
|
# TODO: check localized objects and difficulties (strict list[dict]) and booleans
|
|
|
|
@classmethod
|
|
def items(
|
|
cls, songlist_content: dict, *, skip_asserts: bool = False
|
|
) -> list[Song | SongLocalization | Difficulty | DifficultyLocalization]:
|
|
try:
|
|
cls._assert_root_content(songlist_content)
|
|
except SonglistFormatError as e:
|
|
if skip_asserts:
|
|
logger.exception("Error skipped during songlist parsing:")
|
|
return []
|
|
|
|
raise e
|
|
|
|
songs = songlist_content["songs"]
|
|
results = []
|
|
|
|
for song in songs:
|
|
try:
|
|
cls._assert_song_object(song)
|
|
except SonglistFormatError as e:
|
|
if skip_asserts:
|
|
logger.exception("Error skipped during songlist parsing:")
|
|
continue
|
|
|
|
raise e
|
|
|
|
if song.get("deleted"):
|
|
continue # TODO: none, return...
|
|
|
|
title_localized = song.get("title_localized", {})
|
|
bg_daynight = song.get("bg_daynight", {})
|
|
source_localized = song.get("source_localized", {})
|
|
jacket_localized = song.get("jacket_localized", {})
|
|
|
|
id_ = song["id"]
|
|
|
|
results.append(
|
|
Song(
|
|
pack_id=song["set"],
|
|
id=id_,
|
|
added_at=datetime.fromtimestamp(song["date"], tz=timezone.utc),
|
|
idx=song.get("idx", None),
|
|
title=blank_str_to_none(title_localized.get("en")),
|
|
artist=blank_str_to_none(song.get("artist")),
|
|
is_deleted=song.get("deleted", False),
|
|
version=blank_str_to_none(song.get("version")),
|
|
bpm=blank_str_to_none(song.get("bpm")),
|
|
bpm_base=Decimal(song["bpm_base"])
|
|
if song.get("bpm_base")
|
|
else None,
|
|
is_remote=song.get("remote_dl", False),
|
|
is_unlockable_in_world=song.get("world_unlock", False),
|
|
is_beyond_unlock_state_local=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"),
|
|
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")),
|
|
source=blank_str_to_none(source_localized.get("en")),
|
|
source_copyright=blank_str_to_none(song.get("source_copyright")),
|
|
)
|
|
)
|
|
|
|
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(
|
|
SongLocalization(
|
|
id=id_,
|
|
lang=lang_key,
|
|
title=title_l10n,
|
|
source=source_l10n,
|
|
has_jacket=has_localized_jacket,
|
|
)
|
|
)
|
|
|
|
difficulties = song["difficulties"]
|
|
for difficulty in difficulties:
|
|
difficulty_title_localized = difficulty.get("title_localized", {})
|
|
difficulty_artist_localized = difficulty.get("artist_localized", {})
|
|
rating_class = difficulty["ratingClass"]
|
|
|
|
results.append(
|
|
Difficulty(
|
|
song_id=id_,
|
|
rating_class=rating_class,
|
|
rating=difficulty["rating"],
|
|
is_rating_plus=difficulty.get("ratingPlus", False),
|
|
chart_designer=blank_str_to_none(
|
|
difficulty.get("chartDesigner")
|
|
),
|
|
jacket_designer=blank_str_to_none(
|
|
difficulty.get("jacketDesigner")
|
|
),
|
|
has_overriding_audio=difficulty.get("audioOverride", False),
|
|
has_overriding_jacket=difficulty.get("jacketOverride", False),
|
|
jacket_night=blank_str_to_none(difficulty.get("jacket_night")),
|
|
added_at=datetime.fromtimestamp(
|
|
difficulty["date"], tz=timezone.utc
|
|
)
|
|
if difficulty.get("date")
|
|
else None,
|
|
bg=blank_str_to_none(difficulty.get("bg")),
|
|
version=blank_str_to_none(difficulty.get("version")),
|
|
title=blank_str_to_none(difficulty_title_localized.get("en")),
|
|
artist=blank_str_to_none(difficulty_artist_localized.get("en")),
|
|
is_legacy11=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(
|
|
DifficultyLocalization(
|
|
song_id=id_,
|
|
rating_class=rating_class,
|
|
lang=lang_key,
|
|
title=difficuly_title_l10n,
|
|
artist=difficuly_artist_l10n,
|
|
)
|
|
)
|
|
|
|
return results
|