Compare commits
2 Commits
830e480f6f
...
bcaa1a268e
Author | SHA1 | Date | |
---|---|---|---|
bcaa1a268e
|
|||
155e8b9664
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,3 +1,7 @@
|
|||||||
|
.vscode/
|
||||||
|
.debug/
|
||||||
|
debug*.py
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
@ -16,10 +16,12 @@ You should have received a copy of the GNU General Public License along with
|
|||||||
this program. If not, see <https://www.gnu.org/licenses/>.
|
this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import re
|
import re
|
||||||
from typing import ClassVar, Optional
|
from typing import ClassVar
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ArcaeaApkAsset",
|
"ArcaeaApkAsset",
|
||||||
@ -35,27 +37,28 @@ class ArcaeaApkAsset(metaclass=abc.ABCMeta):
|
|||||||
zip_filename: str
|
zip_filename: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_match(cls, string: str) -> Optional[re.Match]:
|
def _get_match(cls, string: str) -> re.Match | None:
|
||||||
return cls.PATTERN.match(string)
|
return cls.PATTERN.match(string)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def from_zip_filename(
|
def from_zip_filename(
|
||||||
cls, asset_zip_filename: str
|
cls,
|
||||||
) -> Optional["ArcaeaApkAsset"]: ...
|
asset_zip_filename: str,
|
||||||
|
) -> ArcaeaApkAsset | None: ...
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class ArcaeaApkAssetJacket(ArcaeaApkAsset):
|
class ArcaeaApkAssetJacket(ArcaeaApkAsset):
|
||||||
PATTERN = re.compile(
|
PATTERN = re.compile(
|
||||||
r"^assets/songs/(?P<raw_song_id>.*)/(1080_)?(?P<raw_rating_class>\d|base)(_(?P<lang>.*))?\.(jpg|png)"
|
r"^assets/songs/(?P<raw_song_id>.*)/(1080_)?(?P<raw_rating_class>\d|base)(_(?P<lang>.*))?\.(jpg|png)",
|
||||||
)
|
)
|
||||||
song_id: str
|
song_id: str
|
||||||
rating_class: Optional[str]
|
rating_class: str | None
|
||||||
lang: Optional[str]
|
lang: str | None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_zip_filename(cls, asset_zip_filename: str):
|
def from_zip_filename(cls, asset_zip_filename: str) -> ArcaeaApkAssetJacket | None:
|
||||||
match = cls._get_match(asset_zip_filename)
|
match = cls._get_match(asset_zip_filename)
|
||||||
if match is None:
|
if match is None:
|
||||||
return None
|
return None
|
||||||
@ -78,7 +81,10 @@ class ArcaeaApkAssetPartnerIcon(ArcaeaApkAsset):
|
|||||||
char_id: str
|
char_id: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_zip_filename(cls, asset_zip_filename: str):
|
def from_zip_filename(
|
||||||
|
cls,
|
||||||
|
asset_zip_filename: str,
|
||||||
|
) -> ArcaeaApkAssetPartnerIcon | None:
|
||||||
match = cls._get_match(asset_zip_filename)
|
match = cls._get_match(asset_zip_filename)
|
||||||
if match is None:
|
if match is None:
|
||||||
return None
|
return None
|
||||||
@ -95,7 +101,7 @@ class ArcaeaApkAssetList(ArcaeaApkAsset):
|
|||||||
filename: str
|
filename: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_zip_filename(cls, asset_zip_filename: str):
|
def from_zip_filename(cls, asset_zip_filename: str) -> ArcaeaApkAssetList | None:
|
||||||
match = cls._get_match(asset_zip_filename)
|
match = cls._get_match(asset_zip_filename)
|
||||||
if match is None:
|
if match is None:
|
||||||
return None
|
return None
|
||||||
|
@ -1,12 +1,4 @@
|
|||||||
from .defs import (
|
from .defs import ARCAEA_LANGUAGE_KEYS
|
||||||
ARCAEA_LANGUAGE_KEYS,
|
|
||||||
Difficulty,
|
|
||||||
DifficultyLocalization,
|
|
||||||
Pack,
|
|
||||||
PackLocalization,
|
|
||||||
Song,
|
|
||||||
SongLocalization,
|
|
||||||
)
|
|
||||||
from .packlist import ArcaeaPacklistParser
|
from .packlist import ArcaeaPacklistParser
|
||||||
from .songlist import ArcaeaSonglistParser
|
from .songlist import ArcaeaSonglistParser
|
||||||
|
|
||||||
@ -14,10 +6,4 @@ __all__ = [
|
|||||||
"ARCAEA_LANGUAGE_KEYS",
|
"ARCAEA_LANGUAGE_KEYS",
|
||||||
"ArcaeaPacklistParser",
|
"ArcaeaPacklistParser",
|
||||||
"ArcaeaSonglistParser",
|
"ArcaeaSonglistParser",
|
||||||
"Difficulty",
|
|
||||||
"DifficultyLocalization",
|
|
||||||
"Pack",
|
|
||||||
"PackLocalization",
|
|
||||||
"Song",
|
|
||||||
"SongLocalization",
|
|
||||||
]
|
]
|
||||||
|
@ -16,11 +16,15 @@ You should have received a copy of the GNU General Public License along with
|
|||||||
this program. If not, see <https://www.gnu.org/licenses/>.
|
this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Optional
|
|
||||||
|
from typeguard import typechecked
|
||||||
|
|
||||||
|
|
||||||
def is_string_blank(string: str):
|
@typechecked
|
||||||
|
def is_string_blank(string: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if a string equals to a blank string after stripped.
|
Check if a string equals to a blank string after stripped.
|
||||||
|
|
||||||
@ -37,14 +41,11 @@ def is_string_blank(string: str):
|
|||||||
False
|
False
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not isinstance(string, str):
|
|
||||||
msg = "A `str` object is expected"
|
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
return string.strip() == ""
|
return string.strip() == ""
|
||||||
|
|
||||||
|
|
||||||
def blank_str_to_none(string: str):
|
@typechecked
|
||||||
|
def blank_str_to_none(string: str | None) -> str | None:
|
||||||
"""
|
"""
|
||||||
Convert a blank string (see :func:`~is_string_blank`) to None, otherwise return the
|
Convert a blank string (see :func:`~is_string_blank`) to None, otherwise return the
|
||||||
original string.
|
original string.
|
||||||
@ -68,14 +69,11 @@ def blank_str_to_none(string: str):
|
|||||||
if string is None:
|
if string is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not isinstance(string, str):
|
|
||||||
msg = "A `str` object is expected"
|
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
return None if is_string_blank(string) else string
|
return None if is_string_blank(string) else string
|
||||||
|
|
||||||
|
|
||||||
def replace_single_newline(text: Optional[str]):
|
@typechecked
|
||||||
|
def replace_single_newline(text: str | None) -> str | None:
|
||||||
if text is None:
|
if text is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -16,104 +16,4 @@ You should have received a copy of the GNU General Public License along with
|
|||||||
this program. If not, see <https://www.gnu.org/licenses/>.
|
this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
ARCAEA_LANGUAGE_KEYS = ["ja", "ko", "zh-Hans", "zh-Hant"]
|
ARCAEA_LANGUAGE_KEYS = ["ja", "ko", "zh-Hans", "zh-Hant"]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Pack:
|
|
||||||
id: str
|
|
||||||
name: Optional[str] = None
|
|
||||||
description: Optional[str] = None
|
|
||||||
section: Optional[str] = None
|
|
||||||
plus_character: Optional[int] = None
|
|
||||||
append_parent_id: Optional[str] = None
|
|
||||||
is_world_extend: bool = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_append(self):
|
|
||||||
return self.append_parent_id is not None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PackLocalization:
|
|
||||||
id: str
|
|
||||||
lang: str
|
|
||||||
name: Optional[str]
|
|
||||||
description: Optional[str]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Song:
|
|
||||||
pack_id: str
|
|
||||||
id: str
|
|
||||||
added_at: datetime
|
|
||||||
|
|
||||||
idx: Optional[int] = None
|
|
||||||
title: Optional[str] = None
|
|
||||||
artist: Optional[str] = None
|
|
||||||
is_deleted: bool = False
|
|
||||||
|
|
||||||
version: Optional[str] = None
|
|
||||||
|
|
||||||
bpm: Optional[str] = None
|
|
||||||
bpm_base: Optional[Decimal] = None
|
|
||||||
is_remote: bool = False
|
|
||||||
is_unlockable_in_world: bool = False
|
|
||||||
is_beyond_unlock_state_local: bool = False
|
|
||||||
purchase: Optional[str] = None
|
|
||||||
category: Optional[str] = None
|
|
||||||
|
|
||||||
side: Optional[int] = None
|
|
||||||
bg: Optional[str] = None
|
|
||||||
bg_inverse: Optional[str] = None
|
|
||||||
bg_day: Optional[str] = None
|
|
||||||
bg_night: Optional[str] = None
|
|
||||||
|
|
||||||
source: Optional[str] = None
|
|
||||||
source_copyright: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SongLocalization:
|
|
||||||
id: str
|
|
||||||
lang: str
|
|
||||||
title: Optional[str] = None
|
|
||||||
source: Optional[str] = None
|
|
||||||
has_jacket: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Difficulty:
|
|
||||||
song_id: str
|
|
||||||
rating_class: int
|
|
||||||
|
|
||||||
rating: int
|
|
||||||
is_rating_plus: bool = False
|
|
||||||
|
|
||||||
chart_designer: Optional[str] = None
|
|
||||||
jacket_designer: Optional[str] = None
|
|
||||||
|
|
||||||
has_overriding_audio: bool = False
|
|
||||||
has_overriding_jacket: bool = False
|
|
||||||
jacket_night: Optional[str] = None
|
|
||||||
|
|
||||||
added_at: Optional[datetime] = None
|
|
||||||
bg: Optional[str] = None
|
|
||||||
version: Optional[str] = None
|
|
||||||
title: Optional[str] = None
|
|
||||||
artist: Optional[str] = None
|
|
||||||
is_legacy11: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DifficultyLocalization:
|
|
||||||
song_id: str
|
|
||||||
rating_class: int
|
|
||||||
lang: str
|
|
||||||
title: Optional[str]
|
|
||||||
artist: Optional[str]
|
|
||||||
|
@ -16,11 +16,15 @@ You should have received a copy of the GNU General Public License along with
|
|||||||
this program. If not, see <https://www.gnu.org/licenses/>.
|
this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
from arcaea_offline_schemas.dis.v5.dataclass import AodisPack, AodisPackLocalization
|
||||||
|
from typeguard import TypeCheckError, check_type
|
||||||
|
|
||||||
from ._shared import blank_str_to_none
|
from ._shared import blank_str_to_none
|
||||||
from .defs import ARCAEA_LANGUAGE_KEYS, Pack, PackLocalization
|
from .defs import ARCAEA_LANGUAGE_KEYS
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -31,121 +35,145 @@ class PacklistFormatError(Exception):
|
|||||||
|
|
||||||
class ArcaeaPacklistParser:
|
class ArcaeaPacklistParser:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _assert_root_content(root: Any):
|
def check_root_content(root: dict) -> None:
|
||||||
if not isinstance(root, dict):
|
try:
|
||||||
msg = f"packlist root should be `dict`, got {type(root)}"
|
check_type(root, dict)
|
||||||
raise PacklistFormatError(msg)
|
check_type(root["packs"], list[dict])
|
||||||
|
except (KeyError, TypeCheckError) as e:
|
||||||
packs = root.get("packs")
|
raise PacklistFormatError from e
|
||||||
if packs is None:
|
|
||||||
msg = "`packs` field does not exist"
|
|
||||||
raise PacklistFormatError(msg)
|
|
||||||
|
|
||||||
if not isinstance(packs, list):
|
|
||||||
msg = f"`packs` field should be `list`, got {type(packs)}"
|
|
||||||
raise PacklistFormatError(msg)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _assert_pack_object(obj: Any):
|
def check_pack_object(obj: dict) -> None:
|
||||||
if not isinstance(obj, dict):
|
|
||||||
msg = f"A pack object should be `dict`, got {type(obj)}"
|
|
||||||
raise PacklistFormatError(msg)
|
|
||||||
|
|
||||||
id_ = obj.get("id")
|
|
||||||
if not isinstance(id_, str):
|
|
||||||
msg = f"`id` attribute of a pack should be `str`, got value {id_!r}"
|
|
||||||
raise PacklistFormatError(msg)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def items(
|
|
||||||
cls, packlist_content: dict, *, skip_asserts: bool = False
|
|
||||||
) -> list[Pack | PackLocalization]:
|
|
||||||
try:
|
try:
|
||||||
cls._assert_root_content(packlist_content)
|
check_type(obj, dict)
|
||||||
except PacklistFormatError as e:
|
check_type(obj["id"], str)
|
||||||
if skip_asserts:
|
except (KeyError, TypeCheckError) as e:
|
||||||
logger.exception("Error skipped during packlist parsing:")
|
raise PacklistFormatError from e
|
||||||
return []
|
|
||||||
|
|
||||||
raise e
|
@staticmethod
|
||||||
|
def parse_pack(pack: dict) -> AodisPack:
|
||||||
|
id_ = pack["id"]
|
||||||
|
|
||||||
packs = packlist_content["packs"]
|
name_localized = pack.get("name_localized", {})
|
||||||
results: list[Pack | PackLocalization] = [
|
description_localized = pack.get("description_localized", {})
|
||||||
Pack(
|
|
||||||
id="single",
|
|
||||||
name="Memory Archive",
|
|
||||||
description=None,
|
|
||||||
section=None,
|
|
||||||
plus_character=None,
|
|
||||||
append_parent_id=None,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
for pack in packs:
|
name = blank_str_to_none(name_localized.get("en"))
|
||||||
try:
|
description = blank_str_to_none(description_localized.get("en"))
|
||||||
cls._assert_pack_object(pack)
|
section = blank_str_to_none(pack.get("section"))
|
||||||
except PacklistFormatError as e:
|
append_parent_id = pack.get("pack_parent")
|
||||||
if skip_asserts:
|
plus_character = (
|
||||||
logger.exception("Error skipped during packlist parsing:")
|
pack["plus_character"] if pack.get("plus_character", -1) > -1 else None
|
||||||
continue
|
)
|
||||||
|
|
||||||
raise e
|
return AodisPack(
|
||||||
|
id=id_,
|
||||||
|
name=name or "",
|
||||||
|
isWorldExtend=pack.get("is_extend_pack") is True,
|
||||||
|
description=description,
|
||||||
|
section=section,
|
||||||
|
unlocksPartner=plus_character,
|
||||||
|
appendParentId=append_parent_id,
|
||||||
|
)
|
||||||
|
|
||||||
name_localized = pack.get("name_localized", {})
|
@staticmethod
|
||||||
description_localized = pack.get("description_localized", {})
|
def parse_pack_localizations(pack: dict) -> list[AodisPackLocalization]:
|
||||||
|
results = []
|
||||||
|
|
||||||
id_ = pack["id"]
|
name_localized = pack.get("name_localized", {})
|
||||||
name = blank_str_to_none(name_localized.get("en"))
|
description_localized = pack.get("description_localized", {})
|
||||||
description = blank_str_to_none(description_localized.get("en"))
|
|
||||||
section = blank_str_to_none(pack.get("section"))
|
id_ = pack["id"]
|
||||||
append_parent_id = pack.get("pack_parent")
|
|
||||||
plus_character = (
|
for lang_key in ARCAEA_LANGUAGE_KEYS:
|
||||||
pack["plus_character"] if pack.get("plus_character", -1) > -1 else None
|
name_l10n = blank_str_to_none(name_localized.get(lang_key))
|
||||||
|
description_l10n = blank_str_to_none(
|
||||||
|
description_localized.get(lang_key),
|
||||||
)
|
)
|
||||||
|
|
||||||
results.append(
|
if name_l10n or description_l10n:
|
||||||
Pack(
|
results.append(
|
||||||
id=id_,
|
AodisPackLocalization(
|
||||||
name=name,
|
id=id_,
|
||||||
description=description,
|
lang=lang_key,
|
||||||
section=section,
|
name=name_l10n,
|
||||||
plus_character=plus_character,
|
description=description_l10n,
|
||||||
append_parent_id=append_parent_id,
|
),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
for lang_key in ARCAEA_LANGUAGE_KEYS:
|
|
||||||
name_l10n = blank_str_to_none(name_localized.get(lang_key))
|
|
||||||
description_l10n = blank_str_to_none(
|
|
||||||
description_localized.get(lang_key)
|
|
||||||
)
|
|
||||||
|
|
||||||
if name_l10n or description_l10n:
|
|
||||||
results.append(
|
|
||||||
PackLocalization(
|
|
||||||
id=id_,
|
|
||||||
lang=lang_key,
|
|
||||||
name=name_l10n,
|
|
||||||
description=description_l10n,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def packs(cls, packlist_content: dict, *, skip_asserts: bool = False) -> list[Pack]:
|
def items(
|
||||||
|
cls,
|
||||||
|
packlist_content: dict,
|
||||||
|
*,
|
||||||
|
skip_format_checks: bool = False,
|
||||||
|
) -> list[AodisPack | AodisPackLocalization]:
|
||||||
|
try:
|
||||||
|
cls.check_root_content(packlist_content)
|
||||||
|
except PacklistFormatError:
|
||||||
|
if skip_format_checks:
|
||||||
|
logger.exception("Error skipped during packlist parsing:")
|
||||||
|
return []
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
|
packs = packlist_content["packs"]
|
||||||
|
results: list[AodisPack | AodisPackLocalization] = [
|
||||||
|
AodisPack(
|
||||||
|
id="single",
|
||||||
|
name="Memory Archive",
|
||||||
|
isWorldExtend=False,
|
||||||
|
description=None,
|
||||||
|
section=None,
|
||||||
|
unlocksPartner=None,
|
||||||
|
appendParentId=None,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
for pack in packs:
|
||||||
|
try:
|
||||||
|
cls.check_pack_object(pack)
|
||||||
|
except PacklistFormatError:
|
||||||
|
if skip_format_checks:
|
||||||
|
logger.exception("Error skipped during packlist parsing:")
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
|
results.append(cls.parse_pack(pack))
|
||||||
|
results.extend(cls.parse_pack_localizations(pack))
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def packs(
|
||||||
|
cls,
|
||||||
|
packlist_content: dict,
|
||||||
|
*,
|
||||||
|
skip_format_checks: bool = False,
|
||||||
|
) -> list[AodisPack]:
|
||||||
return [
|
return [
|
||||||
item
|
item
|
||||||
for item in cls.items(packlist_content, skip_asserts=skip_asserts)
|
for item in cls.items(
|
||||||
if isinstance(item, Pack)
|
packlist_content,
|
||||||
|
skip_format_checks=skip_format_checks,
|
||||||
|
)
|
||||||
|
if isinstance(item, AodisPack)
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pack_localizations(
|
def pack_localizations(
|
||||||
cls, packlist_content: dict, *, skip_asserts: bool = False
|
cls,
|
||||||
) -> list[PackLocalization]:
|
packlist_content: dict,
|
||||||
|
*,
|
||||||
|
skip_format_checks: bool = False,
|
||||||
|
) -> list[AodisPackLocalization]:
|
||||||
return [
|
return [
|
||||||
item
|
item
|
||||||
for item in cls.items(packlist_content, skip_asserts=skip_asserts)
|
for item in cls.items(
|
||||||
if isinstance(item, PackLocalization)
|
packlist_content,
|
||||||
|
skip_format_checks=skip_format_checks,
|
||||||
|
)
|
||||||
|
if isinstance(item, AodisPackLocalization)
|
||||||
]
|
]
|
||||||
|
@ -16,19 +16,27 @@ You should have received a copy of the GNU General Public License along with
|
|||||||
this program. If not, see <https://www.gnu.org/licenses/>.
|
this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from decimal import Decimal
|
|
||||||
from typing import Any
|
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 ._shared import blank_str_to_none
|
||||||
from .defs import (
|
from .defs import ARCAEA_LANGUAGE_KEYS
|
||||||
ARCAEA_LANGUAGE_KEYS,
|
|
||||||
Difficulty,
|
|
||||||
DifficultyLocalization,
|
|
||||||
Song,
|
|
||||||
SongLocalization,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -39,170 +47,215 @@ class SonglistFormatError(Exception):
|
|||||||
|
|
||||||
class ArcaeaSonglistParser:
|
class ArcaeaSonglistParser:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _assert_root_content(root: Any):
|
def check_root_content_format(root: dict) -> None:
|
||||||
if not isinstance(root, dict):
|
try:
|
||||||
msg = f"songlist root should be `dict`, got {type(root)}"
|
check_type(root, dict)
|
||||||
raise SonglistFormatError(msg)
|
check_type(root["songs"], list)
|
||||||
|
except (KeyError, TypeCheckError) as e:
|
||||||
songs = root.get("songs")
|
raise SonglistFormatError from e
|
||||||
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
|
@staticmethod
|
||||||
def _assert_song_object(obj: Any):
|
def check_song_object_format(song: dict) -> None:
|
||||||
if not isinstance(obj, dict):
|
try:
|
||||||
msg = f"A song object should be `dict`, got {type(obj)}"
|
check_type(song, dict)
|
||||||
raise SonglistFormatError(msg)
|
check_type(song["id"], str)
|
||||||
|
|
||||||
id_ = obj.get("id")
|
if deleted := song.get("deleted"):
|
||||||
if not isinstance(id_, str):
|
check_type(deleted, bool)
|
||||||
msg = f"`id` attribute of a song should be `str`, got value {id_!r}"
|
|
||||||
raise SonglistFormatError(msg)
|
|
||||||
|
|
||||||
is_deleted = obj.get("deleted", False)
|
bg_daynight = song.get("bg_daynight", {})
|
||||||
if not isinstance(is_deleted, bool):
|
bg = blank_str_to_none(song.get("bg"))
|
||||||
msg = f"`deleted` attribute of a song should be `bool`, got value {id_!r}"
|
bg_inverse = blank_str_to_none(song.get("bg_inverse"))
|
||||||
raise SonglistFormatError(msg)
|
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)
|
||||||
|
|
||||||
# TODO: check localized objects and difficulties (strict list[dict]) and booleans
|
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
|
@classmethod
|
||||||
def items(
|
def items(
|
||||||
cls, songlist_content: dict, *, skip_asserts: bool = False
|
cls,
|
||||||
) -> list[Song | SongLocalization | Difficulty | DifficultyLocalization]:
|
songlist_content: dict,
|
||||||
|
*,
|
||||||
|
skip_format_checks: bool = False,
|
||||||
|
) -> list[
|
||||||
|
AodisSong
|
||||||
|
| AodisSongLocalization
|
||||||
|
| AodisDifficulty
|
||||||
|
| AodisDifficultyLocalization
|
||||||
|
]:
|
||||||
try:
|
try:
|
||||||
cls._assert_root_content(songlist_content)
|
cls.check_root_content_format(songlist_content)
|
||||||
except SonglistFormatError as e:
|
except SonglistFormatError:
|
||||||
if skip_asserts:
|
if skip_format_checks:
|
||||||
logger.exception("Error skipped during songlist parsing:")
|
logger.exception("Error skipped during songlist parsing:")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
raise e
|
raise
|
||||||
|
|
||||||
songs = songlist_content["songs"]
|
songs = songlist_content["songs"]
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for song in songs:
|
for song in songs:
|
||||||
try:
|
try:
|
||||||
cls._assert_song_object(song)
|
cls.check_song_object_format(song)
|
||||||
except SonglistFormatError as e:
|
except SonglistFormatError:
|
||||||
if skip_asserts:
|
if skip_format_checks:
|
||||||
logger.exception("Error skipped during songlist parsing:")
|
logger.exception("Error skipped during songlist parsing:")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
raise e
|
raise
|
||||||
|
|
||||||
if song.get("deleted"):
|
if song.get("deleted"):
|
||||||
continue # TODO: none, return...
|
continue
|
||||||
|
|
||||||
title_localized = song.get("title_localized", {})
|
results.extend(cls.parse_song(song))
|
||||||
bg_daynight = song.get("bg_daynight", {})
|
results.extend(cls.parse_difficulties(song))
|
||||||
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
|
return results
|
||||||
|
@ -16,8 +16,11 @@ You should have received a copy of the GNU General Public License along with
|
|||||||
this program. If not, see <https://www.gnu.org/licenses/>.
|
this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import zipfile
|
from __future__ import annotations
|
||||||
from typing import Any, ClassVar, Optional
|
|
||||||
|
from typing import TYPE_CHECKING, ClassVar
|
||||||
|
|
||||||
|
from typeguard import typechecked
|
||||||
|
|
||||||
from .assets import (
|
from .assets import (
|
||||||
ArcaeaApkAsset,
|
ArcaeaApkAsset,
|
||||||
@ -26,6 +29,9 @@ from .assets import (
|
|||||||
ArcaeaApkAssetPartnerIcon,
|
ArcaeaApkAssetPartnerIcon,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import zipfile
|
||||||
|
|
||||||
__all__ = ["ArcaeaApkAssetsProcessor"]
|
__all__ = ["ArcaeaApkAssetsProcessor"]
|
||||||
|
|
||||||
|
|
||||||
@ -36,14 +42,8 @@ class ArcaeaApkAssetsProcessor:
|
|||||||
ArcaeaApkAssetList,
|
ArcaeaApkAssetList,
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _check_zipfile(value: Any):
|
|
||||||
if not isinstance(value, zipfile.ZipFile):
|
|
||||||
msg = f"{value!r} is not a ZipFile!"
|
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_asset(cls, asset_zip_filename: str) -> Optional[ArcaeaApkAsset]:
|
def get_asset(cls, asset_zip_filename: str) -> ArcaeaApkAsset | None:
|
||||||
asset = None
|
asset = None
|
||||||
for asset_type in cls.ASSET_TYPES:
|
for asset_type in cls.ASSET_TYPES:
|
||||||
if asset_ := asset_type.from_zip_filename(asset_zip_filename):
|
if asset_ := asset_type.from_zip_filename(asset_zip_filename):
|
||||||
@ -57,9 +57,8 @@ class ArcaeaApkAssetsProcessor:
|
|||||||
return cls.get_asset(asset_zip_filename) is not None
|
return cls.get_asset(asset_zip_filename) is not None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@typechecked
|
||||||
def get_assets(cls, zf: zipfile.ZipFile) -> dict[zipfile.ZipInfo, ArcaeaApkAsset]:
|
def get_assets(cls, zf: zipfile.ZipFile) -> dict[zipfile.ZipInfo, ArcaeaApkAsset]:
|
||||||
cls._check_zipfile(zf)
|
|
||||||
|
|
||||||
matches = {}
|
matches = {}
|
||||||
for info in zf.infolist():
|
for info in zf.infolist():
|
||||||
filename = info.filename
|
filename = info.filename
|
||||||
|
7
noxfile.py
Normal file
7
noxfile.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import nox
|
||||||
|
|
||||||
|
|
||||||
|
@nox.session
|
||||||
|
def tests(session: nox.Session) -> None:
|
||||||
|
session.install("pytest")
|
||||||
|
session.run("pytest", "--doctest-modules", "arcaea_apk_assets")
|
@ -8,27 +8,28 @@ build-backend = "setuptools.build_meta"
|
|||||||
# dynamic = ["version"]
|
# dynamic = ["version"]
|
||||||
name = "arcaea-apk-assets"
|
name = "arcaea-apk-assets"
|
||||||
description = "Your package description goes here."
|
description = "Your package description goes here."
|
||||||
version = "0.1.0a"
|
version = "0.1.0a0.dev2"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
authors = [
|
authors = [{ name = "283375", email = "log_283375@163.com" }]
|
||||||
{name = "283375", email = "log_283375@163.com"},
|
|
||||||
]
|
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
|
dependencies = ["typeguard~=4.4"]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/283375/arcaea-apk-assets"
|
||||||
|
Issues = "https://github.com/283375/arcaea-apk-assets/issues"
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = [
|
select = ["ALL"]
|
||||||
"E", # pycodestyle
|
ignore = ["D"]
|
||||||
"F", # Pyflakes
|
|
||||||
"UP", # pyupgrade
|
[tool.ruff.lint.extend-per-file-ignores]
|
||||||
"B", # flake8-bugbear
|
"tests/**/*.py" = [
|
||||||
"SIM", # flake8-simplify
|
# Disable certain rules for tests
|
||||||
"I", # isort
|
# https://github.com/astral-sh/ruff/issues/4368#issuecomment-2245567481
|
||||||
"RET", # flake8-return
|
"S101", # asserts are allowed in tests
|
||||||
"EM", # flake8-errmsg
|
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||||
"CPY", # flake8-copyright
|
"ARG", # Unused function args: fixtures nevertheless are functionally relevant
|
||||||
"RUF", # Ruff-specific rules
|
"FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize()
|
||||||
]
|
"PLR2004", # Magic value used in comparison
|
||||||
ignore = [
|
|
||||||
"E501", # line-too-long
|
|
||||||
]
|
]
|
||||||
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/contents/__init__.py
Normal file
0
tests/contents/__init__.py
Normal file
52
tests/contents/packlist.py
Normal file
52
tests/contents/packlist.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# ruff: noqa: RUF001
|
||||||
|
|
||||||
|
NORMAL = {
|
||||||
|
"packs": [
|
||||||
|
{
|
||||||
|
"id": "test",
|
||||||
|
"section": "mainstory",
|
||||||
|
"is_extend_pack": False,
|
||||||
|
"is_active_extend_pack": False,
|
||||||
|
"custom_banner": True,
|
||||||
|
"small_pack_image": False,
|
||||||
|
"plus_character": -1,
|
||||||
|
"name_localized": {"en": "Test Collection 1"},
|
||||||
|
"description_localized": {
|
||||||
|
"en": "DIVINE GRACE, AWAITS YOU IN A LOST WORLD, OF SILENT SONG...",
|
||||||
|
"ja": "「光」も「対立」も全都有、「钙麻」失われた世界で、あなたを待ち受ける⋯",
|
||||||
|
"ko": "고통이 고통이라는 이유로 그 자체를 사랑하고 소유하려는 자는 없다",
|
||||||
|
"zh-Hant": "神的恩典,在一個失去的世界等你,介詞安靜的歌曲……",
|
||||||
|
"zh-Hans": "神的恩典,在一个失去的世界等你,介词安静的歌曲……",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "test_other_properties",
|
||||||
|
"section": "archive",
|
||||||
|
"is_extend_pack": True,
|
||||||
|
"is_active_extend_pack": False,
|
||||||
|
"custom_banner": True,
|
||||||
|
"small_pack_image": True,
|
||||||
|
"plus_character": 123,
|
||||||
|
"name_localized": {"en": "Test Collection 1 - Archive"},
|
||||||
|
"description_localized": {
|
||||||
|
"en": "A FADING LIGHT, AWAITS YOU IN A PURE MEMORY WORLD, OF MUSICAL CONFLICT...",
|
||||||
|
"ja": "「消えゆく光」が、「ピュア・メモリー」に満ちた世界が、あなたを待ち受ける⋯",
|
||||||
|
"ko": "혹은 아무런 즐거움도 생기지 않는 고통을 회피하는 사람을 누가 탓할 수 있겠는가?",
|
||||||
|
"zh-Hant": "一個消色的光,在一個“純 淨 回 憶”的世界等你,介詞音樂的衝突……",
|
||||||
|
"zh-Hans": "一个消色的光,在一个“纯 净 回 忆”的世界等你,介词音乐的冲突……",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
MALFORMED_WTF = {
|
||||||
|
"wow": [],
|
||||||
|
"random": {},
|
||||||
|
"parts!": "huh?",
|
||||||
|
}
|
||||||
|
MALFORMED_ROOT = {
|
||||||
|
"packs": {
|
||||||
|
"pack1": "something",
|
||||||
|
"pack2": "otherthing",
|
||||||
|
},
|
||||||
|
}
|
37
tests/test_packlist.py
Normal file
37
tests/test_packlist.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from arcaea_apk_assets.parsers.packlist import ArcaeaPacklistParser, PacklistFormatError
|
||||||
|
from tests.contents import packlist as contents
|
||||||
|
|
||||||
|
|
||||||
|
class TestPacklistParser:
|
||||||
|
def test_normal(self):
|
||||||
|
content = contents.NORMAL
|
||||||
|
|
||||||
|
ArcaeaPacklistParser.packs(content)
|
||||||
|
|
||||||
|
def test_malformed(self):
|
||||||
|
with pytest.raises(PacklistFormatError):
|
||||||
|
ArcaeaPacklistParser.items(contents.MALFORMED_WTF)
|
||||||
|
|
||||||
|
with pytest.raises(PacklistFormatError):
|
||||||
|
ArcaeaPacklistParser.items(contents.MALFORMED_ROOT)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
len(
|
||||||
|
ArcaeaPacklistParser.items(
|
||||||
|
contents.MALFORMED_WTF,
|
||||||
|
skip_format_checks=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
== 0
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
len(
|
||||||
|
ArcaeaPacklistParser.items(
|
||||||
|
contents.MALFORMED_ROOT,
|
||||||
|
skip_format_checks=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
== 0
|
||||||
|
)
|
Reference in New Issue
Block a user