init
This commit is contained in:
15
arcaea_apk_assets/__init__.py
Normal file
15
arcaea_apk_assets/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
from .aseets import (
|
||||
ArcaeaApkAsset,
|
||||
ArcaeaApkAssetJacket,
|
||||
ArcaeaApkAssetList,
|
||||
ArcaeaApkAssetPartnerIcon,
|
||||
)
|
||||
from .processor import ArcaeaApkAssetsProcessor
|
||||
|
||||
__all__ = [
|
||||
"ArcaeaApkAsset",
|
||||
"ArcaeaApkAssetJacket",
|
||||
"ArcaeaApkAssetList",
|
||||
"ArcaeaApkAssetPartnerIcon",
|
||||
"ArcaeaApkAssetsProcessor",
|
||||
]
|
106
arcaea_apk_assets/aseets.py
Normal file
106
arcaea_apk_assets/aseets.py
Normal file
@ -0,0 +1,106 @@
|
||||
"""
|
||||
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 abc
|
||||
import dataclasses
|
||||
import re
|
||||
from typing import ClassVar, Optional
|
||||
|
||||
__all__ = [
|
||||
"ArcaeaApkAsset",
|
||||
"ArcaeaApkAssetJacket",
|
||||
"ArcaeaApkAssetList",
|
||||
"ArcaeaApkAssetPartnerIcon",
|
||||
]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ArcaeaApkAsset(metaclass=abc.ABCMeta):
|
||||
PATTERN: ClassVar[re.Pattern]
|
||||
zip_filename: str
|
||||
|
||||
@classmethod
|
||||
def _get_match(cls, string: str) -> Optional[re.Match]:
|
||||
return cls.PATTERN.match(string)
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def from_zip_filename(
|
||||
cls, asset_zip_filename: str
|
||||
) -> Optional["ArcaeaApkAsset"]: ...
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ArcaeaApkAssetJacket(ArcaeaApkAsset):
|
||||
PATTERN = re.compile(
|
||||
r"^assets/songs/(?P<raw_song_id>.*)/(1080_)?(?P<raw_rating_class>\d|base)(_(?P<lang>.*))?\.(jpg|png)"
|
||||
)
|
||||
song_id: str
|
||||
rating_class: Optional[str]
|
||||
lang: Optional[str]
|
||||
|
||||
@classmethod
|
||||
def from_zip_filename(cls, asset_zip_filename: str):
|
||||
match = cls._get_match(asset_zip_filename)
|
||||
if match is None:
|
||||
return None
|
||||
|
||||
return cls(
|
||||
zip_filename=asset_zip_filename,
|
||||
song_id=match.group("raw_song_id").replace("dl_", ""),
|
||||
rating_class=(
|
||||
None
|
||||
if match.group("raw_rating_class") == "base"
|
||||
else match.group("raw_rating_class")
|
||||
),
|
||||
lang=match.group("lang"),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ArcaeaApkAssetPartnerIcon(ArcaeaApkAsset):
|
||||
PATTERN = re.compile(r"^assets/char/(?P<char_id>\d+u?)_icon\.(jpg|png)")
|
||||
char_id: str
|
||||
|
||||
@classmethod
|
||||
def from_zip_filename(cls, asset_zip_filename: str):
|
||||
match = cls._get_match(asset_zip_filename)
|
||||
if match is None:
|
||||
return None
|
||||
|
||||
return cls(
|
||||
zip_filename=asset_zip_filename,
|
||||
char_id=match.group("char_id"),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ArcaeaApkAssetList(ArcaeaApkAsset):
|
||||
PATTERN = re.compile(r"^assets/songs/(?P<filename>(pack|song)list|unlocks)")
|
||||
filename: str
|
||||
|
||||
@classmethod
|
||||
def from_zip_filename(cls, asset_zip_filename: str):
|
||||
match = cls._get_match(asset_zip_filename)
|
||||
if match is None:
|
||||
return None
|
||||
|
||||
return cls(
|
||||
zip_filename=asset_zip_filename,
|
||||
filename=match.group("filename"),
|
||||
)
|
23
arcaea_apk_assets/parsers/__init__.py
Normal file
23
arcaea_apk_assets/parsers/__init__.py
Normal file
@ -0,0 +1,23 @@
|
||||
from .defs import (
|
||||
ARCAEA_LANGUAGE_KEYS,
|
||||
Difficulty,
|
||||
DifficultyLocalization,
|
||||
Pack,
|
||||
PackLocalization,
|
||||
Song,
|
||||
SongLocalization,
|
||||
)
|
||||
from .packlist import ArcaeaPacklistParser
|
||||
from .songlist import ArcaeaSonglistParser
|
||||
|
||||
__all__ = [
|
||||
"ARCAEA_LANGUAGE_KEYS",
|
||||
"ArcaeaPacklistParser",
|
||||
"ArcaeaSonglistParser",
|
||||
"Difficulty",
|
||||
"DifficultyLocalization",
|
||||
"Pack",
|
||||
"PackLocalization",
|
||||
"Song",
|
||||
"SongLocalization",
|
||||
]
|
82
arcaea_apk_assets/parsers/_shared.py
Normal file
82
arcaea_apk_assets/parsers/_shared.py
Normal file
@ -0,0 +1,82 @@
|
||||
"""
|
||||
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 re
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def is_string_blank(string: str):
|
||||
"""
|
||||
Check if a string equals to a blank string after stripped.
|
||||
|
||||
>>> is_string_blank("abc")
|
||||
False
|
||||
|
||||
>>> is_string_blank("")
|
||||
True
|
||||
|
||||
>>> is_string_blank(" ")
|
||||
True
|
||||
|
||||
>>> is_string_blank(" a ")
|
||||
False
|
||||
"""
|
||||
|
||||
if not isinstance(string, str):
|
||||
msg = "A `str` object is expected"
|
||||
raise TypeError(msg)
|
||||
|
||||
return string.strip() == ""
|
||||
|
||||
|
||||
def blank_str_to_none(string: str):
|
||||
"""
|
||||
Convert a blank string (see :func:`~is_string_blank`) to None, otherwise return the
|
||||
original string.
|
||||
|
||||
>>> blank_str_to_none("") is None
|
||||
True
|
||||
|
||||
>>> blank_str_to_none("abc") == "abc"
|
||||
True
|
||||
|
||||
>>> blank_str_to_none(" ") is None
|
||||
True
|
||||
|
||||
>>> blank_str_to_none(" abc ") == " abc "
|
||||
True
|
||||
|
||||
>>> blank_str_to_none(None) is None
|
||||
True
|
||||
"""
|
||||
|
||||
if string is 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
|
||||
|
||||
|
||||
def replace_single_newline(text: Optional[str]):
|
||||
if text is None:
|
||||
return None
|
||||
|
||||
return re.sub(r"\n+", lambda m: "\n" * (len(m.group()) - 1), text)
|
118
arcaea_apk_assets/parsers/defs.py
Normal file
118
arcaea_apk_assets/parsers/defs.py
Normal file
@ -0,0 +1,118 @@
|
||||
"""
|
||||
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 dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
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
|
||||
is_legacy11: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class DifficultyLocalization:
|
||||
song_id: str
|
||||
rating_class: int
|
||||
lang: str
|
||||
title: Optional[str]
|
||||
artist: Optional[str]
|
151
arcaea_apk_assets/parsers/packlist.py
Normal file
151
arcaea_apk_assets/parsers/packlist.py
Normal file
@ -0,0 +1,151 @@
|
||||
"""
|
||||
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 typing import Any
|
||||
|
||||
from ._shared import blank_str_to_none
|
||||
from .defs import ARCAEA_LANGUAGE_KEYS, Pack, PackLocalization
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PacklistFormatError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ArcaeaPacklistParser:
|
||||
@staticmethod
|
||||
def _assert_root_content(root: Any):
|
||||
if not isinstance(root, dict):
|
||||
msg = f"packlist root should be `dict`, got {type(root)}"
|
||||
raise PacklistFormatError(msg)
|
||||
|
||||
packs = root.get("packs")
|
||||
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
|
||||
def _assert_pack_object(obj: Any):
|
||||
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:
|
||||
cls._assert_root_content(packlist_content)
|
||||
except PacklistFormatError as e:
|
||||
if skip_asserts:
|
||||
logger.exception("Error skipped during packlist parsing:")
|
||||
return []
|
||||
|
||||
raise e
|
||||
|
||||
packs = packlist_content["packs"]
|
||||
results: list[Pack | PackLocalization] = [
|
||||
Pack(
|
||||
id="single",
|
||||
name="Memory Archive",
|
||||
description=None,
|
||||
section=None,
|
||||
plus_character=None,
|
||||
append_parent_id=None,
|
||||
)
|
||||
]
|
||||
|
||||
for pack in packs:
|
||||
try:
|
||||
cls._assert_pack_object(pack)
|
||||
except PacklistFormatError as e:
|
||||
if skip_asserts:
|
||||
logger.exception("Error skipped during packlist parsing:")
|
||||
continue
|
||||
|
||||
raise e
|
||||
|
||||
name_localized = pack.get("name_localized", {})
|
||||
description_localized = pack.get("description_localized", {})
|
||||
|
||||
id_ = pack["id"]
|
||||
name = blank_str_to_none(name_localized.get("en"))
|
||||
description = blank_str_to_none(description_localized.get("en"))
|
||||
section = blank_str_to_none(pack.get("section"))
|
||||
append_parent_id = pack.get("pack_parent")
|
||||
plus_character = (
|
||||
pack["plus_character"] if pack.get("plus_character", -1) > -1 else None
|
||||
)
|
||||
|
||||
results.append(
|
||||
Pack(
|
||||
id=id_,
|
||||
name=name,
|
||||
description=description,
|
||||
section=section,
|
||||
plus_character=plus_character,
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def packs(cls, packlist_content: dict, *, skip_asserts: bool = False) -> list[Pack]:
|
||||
return [
|
||||
item
|
||||
for item in cls.items(packlist_content, skip_asserts=skip_asserts)
|
||||
if isinstance(item, Pack)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def pack_localizations(
|
||||
cls, packlist_content: dict, *, skip_asserts: bool = False
|
||||
) -> list[PackLocalization]:
|
||||
return [
|
||||
item
|
||||
for item in cls.items(packlist_content, skip_asserts=skip_asserts)
|
||||
if isinstance(item, PackLocalization)
|
||||
]
|
208
arcaea_apk_assets/parsers/songlist.py
Normal file
208
arcaea_apk_assets/parsers/songlist.py
Normal file
@ -0,0 +1,208 @@
|
||||
"""
|
||||
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
|
70
arcaea_apk_assets/processor.py
Normal file
70
arcaea_apk_assets/processor.py
Normal file
@ -0,0 +1,70 @@
|
||||
"""
|
||||
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 zipfile
|
||||
from typing import Any, ClassVar, Optional
|
||||
|
||||
from .aseets import (
|
||||
ArcaeaApkAsset,
|
||||
ArcaeaApkAssetJacket,
|
||||
ArcaeaApkAssetList,
|
||||
ArcaeaApkAssetPartnerIcon,
|
||||
)
|
||||
|
||||
__all__ = ["ArcaeaApkAssetsProcessor"]
|
||||
|
||||
|
||||
class ArcaeaApkAssetsProcessor:
|
||||
ASSET_TYPES: ClassVar[list[type[ArcaeaApkAsset]]] = [
|
||||
ArcaeaApkAssetJacket,
|
||||
ArcaeaApkAssetPartnerIcon,
|
||||
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
|
||||
def get_asset(cls, asset_zip_filename: str) -> Optional[ArcaeaApkAsset]:
|
||||
asset = None
|
||||
for asset_type in cls.ASSET_TYPES:
|
||||
if asset_ := asset_type.from_zip_filename(asset_zip_filename):
|
||||
asset = asset_
|
||||
break
|
||||
|
||||
return asset
|
||||
|
||||
@classmethod
|
||||
def is_asset(cls, asset_zip_filename: str) -> bool:
|
||||
return cls.get_asset(asset_zip_filename) is not None
|
||||
|
||||
@classmethod
|
||||
def get_assets(cls, zf: zipfile.ZipFile) -> dict[zipfile.ZipInfo, ArcaeaApkAsset]:
|
||||
cls._check_zipfile(zf)
|
||||
|
||||
matches = {}
|
||||
for info in zf.infolist():
|
||||
filename = info.filename
|
||||
asset = cls.get_asset(filename)
|
||||
if asset is not None:
|
||||
matches[info] = asset
|
||||
|
||||
return matches
|
Reference in New Issue
Block a user