""" 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 . """ 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