init
This commit is contained in:
commit
47839d32f4
160
.gitignore
vendored
Normal file
160
.gitignore
vendored
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
13
LICENSE
Normal file
13
LICENSE
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
Version 2, December 2004
|
||||||
|
|
||||||
|
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim or modified
|
||||||
|
copies of this license document, and changing it is allowed as long
|
||||||
|
as the name is changed.
|
||||||
|
|
||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. You just DO WHAT THE FUCK YOU WANT TO.
|
7
README.md
Normal file
7
README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# arcaea-apk-extract
|
||||||
|
|
||||||
|
Literal meaning.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
arcaea-apk-extract is licensed under the WTFPL. You may have received a copy of the WTFPL along with arcaea-apk-extract, but whatever nobody cares that shit. Feel free to fuck anything.
|
139
andreal.py
Normal file
139
andreal.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path, PurePath
|
||||||
|
|
||||||
|
from common import ArcaeaApkParser, ExtractTask, extract
|
||||||
|
|
||||||
|
ROOT_OUTPUT_PATH = Path(sys.argv[0]).parent.absolute()
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="arcaea-apk-extract for Andreal",
|
||||||
|
description="Literal meaning.",
|
||||||
|
epilog="This program is licensed under the WTFPL. Feel free to fuck anything.",
|
||||||
|
)
|
||||||
|
parser.add_argument("apk_file")
|
||||||
|
parser.add_argument(
|
||||||
|
"-op",
|
||||||
|
"--output-path",
|
||||||
|
action="store",
|
||||||
|
help="output path, defaults to Path(sys.argv[0]).parent",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-v",
|
||||||
|
"--verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="set logging level to logging.DEBUG",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SongParser(ArcaeaApkParser):
|
||||||
|
@property
|
||||||
|
def OUTPUT_PATH(self):
|
||||||
|
return ROOT_OUTPUT_PATH / "Song"
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
songs_dir_zf_path = zipfile.Path(self.zf) / "assets" / "songs"
|
||||||
|
songs_zf_path = [zfp for zfp in songs_dir_zf_path.iterdir() if zfp.is_dir()]
|
||||||
|
|
||||||
|
tasks = []
|
||||||
|
for song_zf_path in songs_zf_path:
|
||||||
|
for file_zf_path in song_zf_path.iterdir():
|
||||||
|
if not file_zf_path.is_file():
|
||||||
|
continue
|
||||||
|
|
||||||
|
file_pure_path = PurePath(str(file_zf_path))
|
||||||
|
|
||||||
|
if file_pure_path.suffix not in [".jpg", ".png"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
song_id = re.sub(r"^dl_", "", song_zf_path.name)
|
||||||
|
if file_pure_path.stem == "base":
|
||||||
|
new_file_name_stem = song_id
|
||||||
|
elif file_pure_path.stem == "base_night":
|
||||||
|
new_file_name_stem = f"{song_id}_night"
|
||||||
|
elif file_pure_path.stem in ["0", "1", "2", "3"]:
|
||||||
|
new_file_name_stem = f"{song_id}_{file_pure_path.stem}"
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_file_name = f"{new_file_name_stem}{file_pure_path.suffix}"
|
||||||
|
new_file_path = self.OUTPUT_PATH / new_file_name
|
||||||
|
|
||||||
|
tasks.append(ExtractTask(file_zf_path, new_file_path))
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
|
class CharParser(ArcaeaApkParser):
|
||||||
|
CHAR_RE = r"^\d*u?(_icon)?\.png$"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def OUTPUT_PATH_CHAR(self):
|
||||||
|
return ROOT_OUTPUT_PATH / "Char"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def OUTPUT_PATH_ICON(self):
|
||||||
|
return ROOT_OUTPUT_PATH / "Icon"
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
char_dir_zf_path = zipfile.Path(self.zf) / "assets" / "char"
|
||||||
|
char_zf_path_list = [zfp for zfp in char_dir_zf_path.iterdir() if zfp.is_file()]
|
||||||
|
|
||||||
|
tasks = []
|
||||||
|
for char_zf_path in char_zf_path_list:
|
||||||
|
char_pure_path = PurePath(str(char_zf_path))
|
||||||
|
if not re.match(self.CHAR_RE, char_pure_path.name):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "_icon" in char_pure_path.stem:
|
||||||
|
new_file_name = char_pure_path.name.replace("_icon", "")
|
||||||
|
new_file_path = self.OUTPUT_PATH_ICON / new_file_name
|
||||||
|
else:
|
||||||
|
new_file_name = char_pure_path.name
|
||||||
|
new_file_path = self.OUTPUT_PATH_CHAR / new_file_name
|
||||||
|
|
||||||
|
tasks.append(ExtractTask(char_zf_path, new_file_path))
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args = parser.parse_args(sys.argv[1:])
|
||||||
|
|
||||||
|
if args.output_path:
|
||||||
|
output_path = Path(args.output_path)
|
||||||
|
assert output_path.exists()
|
||||||
|
ROOT_OUTPUT_PATH = output_path
|
||||||
|
|
||||||
|
logging_level = logging.DEBUG if args.verbose else logging.INFO
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging_level,
|
||||||
|
style="{",
|
||||||
|
format="[{levelname}]{name}: {msg}",
|
||||||
|
stream=sys.stdout,
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
apk_path = Path(args.apk_file)
|
||||||
|
assert apk_path.exists()
|
||||||
|
apk_zf = zipfile.ZipFile(str(apk_path))
|
||||||
|
|
||||||
|
sp = SongParser(apk_zf)
|
||||||
|
cp = CharParser(apk_zf)
|
||||||
|
|
||||||
|
sp.OUTPUT_PATH.mkdir(parents=True, exist_ok=True)
|
||||||
|
cp.OUTPUT_PATH_CHAR.mkdir(parents=True, exist_ok=True)
|
||||||
|
cp.OUTPUT_PATH_ICON.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
sp_tasks = sp.parse()
|
||||||
|
logger.info(f"{len(sp_tasks)} files from songs")
|
||||||
|
cp_tasks = cp.parse()
|
||||||
|
logger.info(f"{len(cp_tasks)} files from char")
|
||||||
|
|
||||||
|
tasks = sp_tasks + cp_tasks
|
||||||
|
logger.info("Extracting, please wait...")
|
||||||
|
for task in tasks:
|
||||||
|
extract(task)
|
30
common.py
Normal file
30
common.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import logging
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path, PurePath
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractTask(NamedTuple):
|
||||||
|
old_path: zipfile.Path
|
||||||
|
new_path: Path | PurePath
|
||||||
|
|
||||||
|
|
||||||
|
def extract(task: ExtractTask):
|
||||||
|
logger.debug(f"{task.old_path} -> {task.new_path}")
|
||||||
|
with task.old_path.open("rb") as src_file:
|
||||||
|
with Path(task.new_path).open("wb") as target_file:
|
||||||
|
while True:
|
||||||
|
if chunk := src_file.read(4096):
|
||||||
|
target_file.write(chunk)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
class ArcaeaApkParser:
|
||||||
|
def __init__(self, zf: zipfile.ZipFile):
|
||||||
|
self.zf = zf
|
||||||
|
|
||||||
|
def parse(self) -> list[ExtractTask]:
|
||||||
|
raise NotImplementedError()
|
Loading…
x
Reference in New Issue
Block a user