azlassets 3.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- azlassets-3.3.0/LICENSE +21 -0
- azlassets-3.3.0/PKG-INFO +108 -0
- azlassets-3.3.0/README.md +70 -0
- azlassets-3.3.0/pyproject.toml +56 -0
- azlassets-3.3.0/setup.cfg +4 -0
- azlassets-3.3.0/src/azlassets/__init__.py +1 -0
- azlassets-3.3.0/src/azlassets/classes.py +191 -0
- azlassets-3.3.0/src/azlassets/config/client_config.json +27 -0
- azlassets-3.3.0/src/azlassets/config/user_config_template.yml +61 -0
- azlassets-3.3.0/src/azlassets/config.py +62 -0
- azlassets-3.3.0/src/azlassets/downloader.py +61 -0
- azlassets-3.3.0/src/azlassets/imgrecon.py +38 -0
- azlassets-3.3.0/src/azlassets/proto/__init__.py +0 -0
- azlassets-3.3.0/src/azlassets/proto/p10min_pb_pb2.py +28 -0
- azlassets-3.3.0/src/azlassets/protobuf.py +116 -0
- azlassets-3.3.0/src/azlassets/repair.py +98 -0
- azlassets-3.3.0/src/azlassets/updater.py +137 -0
- azlassets-3.3.0/src/azlassets/versioncontrol.py +131 -0
- azlassets-3.3.0/src/azlassets.egg-info/PKG-INFO +108 -0
- azlassets-3.3.0/src/azlassets.egg-info/SOURCES.txt +24 -0
- azlassets-3.3.0/src/azlassets.egg-info/dependency_links.txt +1 -0
- azlassets-3.3.0/src/azlassets.egg-info/requires.txt +9 -0
- azlassets-3.3.0/src/azlassets.egg-info/top_level.txt +4 -0
- azlassets-3.3.0/src/downloader.py +61 -0
- azlassets-3.3.0/src/extractor.py +161 -0
- azlassets-3.3.0/src/obb_apk_import.py +192 -0
azlassets-3.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 nobbyfix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
azlassets-3.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: azlassets
|
|
3
|
+
Version: 3.3.0
|
|
4
|
+
Summary: Azur Lane asset downloader and extractor
|
|
5
|
+
Author: nobbyfix
|
|
6
|
+
Project-URL: homepage, https://github.com/nobbyfix/AzurLane-AssetDownloader
|
|
7
|
+
Project-URL: repository, https://github.com/nobbyfix/AzurLane-AssetDownloader.git
|
|
8
|
+
Project-URL: issues, https://github.com/nobbyfix/AzurLane-AssetDownloader/issues
|
|
9
|
+
Project-URL: changelog, https://github.com/nobbyfix/AzurLane-AssetDownloader/blob/master/CHANGELOG.md
|
|
10
|
+
Project-URL: download, https://github.com/nobbyfix/AzurLane-AssetDownloader/releases/latest
|
|
11
|
+
Keywords: azurlane,azur-lane
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
25
|
+
Classifier: Topic :: Games/Entertainment
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: UnityPy>=1.10.18
|
|
30
|
+
Requires-Dist: PyYAML
|
|
31
|
+
Requires-Dist: Pillow
|
|
32
|
+
Requires-Dist: protobuf
|
|
33
|
+
Requires-Dist: aiofile
|
|
34
|
+
Requires-Dist: aiohttp[speedups]
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# Azur Lane Asset Downloader and Extractor
|
|
40
|
+
This tool automatically downloads the newest assets directly from the game's CDN servers and allows extraction of Texture2D files as PNG images.
|
|
41
|
+
|
|
42
|
+
## Upgrade Notice
|
|
43
|
+
### From 2.x / no version number to 3.x+
|
|
44
|
+
When upgrading from versions 2.x or with no version number, the project has to be newly set up. To retain all current data the following folders can be copied to the new folder of the project:
|
|
45
|
+
- `config`: Only user_config.yml is required, the rest can be deleted.
|
|
46
|
+
- `ClientAssets` or directory set in `asset-directory` of the config: Contains all currently downloaded assets, version information und update logs used for extraction. Highly recommend to transfer to the new project folder.
|
|
47
|
+
|
|
48
|
+
## Setup
|
|
49
|
+
Before installation, Python 3.11 or newer needs to be available on the system. It is recommended to set the project up using [venv](https://docs.python.org/3/tutorial/venv.html) or a similar virtual environment manager. The project can be installed using pip:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
pip install azlassets
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Alternatively, to install the newest version from the repository (requires git on the system):
|
|
56
|
+
```
|
|
57
|
+
pip install git+https://github.com/nobbyfix/AzurLane-AssetDownloader.git
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
There are three scripts to manage the assets:
|
|
62
|
+
- `obb_apk_import`: Importing assets from obb/apk/xapk files
|
|
63
|
+
- `downlader`: Downloading assets from the game server
|
|
64
|
+
- `extractor`: Extract PNGs from the assets
|
|
65
|
+
|
|
66
|
+
These can be executed using `py -m <scriptname>` on Windows or `python3 -m <scriptname>` on Linux/macOS (will be shortened to `py[thon3]` going forward, use the appropriate version for your system). Detailed usage will be explained in the following sections.
|
|
67
|
+
|
|
68
|
+
### 1. Import files from xapk/apk/obb
|
|
69
|
+
While this is *not necessary*, this step is **recommended** if you want all game assets available and not spam the game update server with errors of missing files on the first download.
|
|
70
|
+
|
|
71
|
+
The `obb_apk_import.py` supports all game clients (EN, JP, CN, KR, TW) and multiple forms of importing the assets. The recommended and easiest way is by downloading the `.xapk` from one of many Google Play Store app distributors (like APKMirror or APKPure). You can find them by searching for the package name, which are as follows:
|
|
72
|
+
- EN: com.YoStarEN.AzurLane
|
|
73
|
+
- JP: com.YoStarJP.AzurLane
|
|
74
|
+
- KR: kr.txwy.and.blhx
|
|
75
|
+
- TW: com.hkmanjuu.azurlane.gp
|
|
76
|
+
|
|
77
|
+
Alternatively if you already have the game installed, for example on emulators, you can copy the obb file onto your system and use it instead of the xapk. On Android it can be found in the folder `/storage/emulated/0/Android/obb/[PACKAGE_NAME]/`.
|
|
78
|
+
|
|
79
|
+
Since the CN client is not distributed through the Google Play Store, there is no xapk/obb file for it, but you can find the android download link on the [website](https://game.bilibili.com/blhx/) which will download an apk file (not xapk like the others). Alternatively, the APK is installed in the folder `/data/app/com.bilibili.azurlane-1/` on android (Note: Root access is required to access this folder).
|
|
80
|
+
|
|
81
|
+
You can then execute the script by passing it the filepath to the xapk/apk/obb:
|
|
82
|
+
```
|
|
83
|
+
py[thon3] -m obb_apk_import [FILEPATH]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Settings
|
|
87
|
+
The `config/user_config.yml` file provides a few settings to filter which files will be downloaded (and later also extracted). The options `download-folder-listtype` and `extract-folder-listtype` can be set to either "blacklist" or "whitelist". Depending on this it will filter by the top-level folder names (subfolders are not supported) or top-level filenames (files inside top-level folders or lower cannot be filtered) set in `download-folder-list` and `extract-folder-list`. This allows to cut down the download and extraction times by skipping unneeded assets.
|
|
88
|
+
|
|
89
|
+
### 3. Download new updates from the game
|
|
90
|
+
All assets normally distributed via the in-app downloader can be downloaded by simply executing:
|
|
91
|
+
```
|
|
92
|
+
py[thon3] -m downloader [CLIENT]
|
|
93
|
+
```
|
|
94
|
+
where `CLIENT` has to be either EN, CN, JP, KR or TW. You can check which files have been downloaded or deleted using the difflog files in `ClientAssets/[CLIENT]/difflog`.
|
|
95
|
+
|
|
96
|
+
### 4. Extract all new and changed files
|
|
97
|
+
The asset extraction script supports extraction of all newly downloaded files and single asset bundles. The newly downloaded assets can be extracted by executing:
|
|
98
|
+
```
|
|
99
|
+
py[thon3] -m extractor [CLIENT]
|
|
100
|
+
```
|
|
101
|
+
where `CLIENT` is again one of EN, CN, JP, KR or TW. The extracted images will then be saved in `ClientExtract/[CLIENT]/` Since only Texture2D assets are exported, it's not desired to try to export from all assetbundles (See [settings section](#2-settings)).
|
|
102
|
+
|
|
103
|
+
A single assetbundle can be extracted by passing the filepath to the script:
|
|
104
|
+
```
|
|
105
|
+
py[thon3] -m extractor -f [FILEPATH]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 5. Enjoy the files
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Azur Lane Asset Downloader and Extractor
|
|
2
|
+
This tool automatically downloads the newest assets directly from the game's CDN servers and allows extraction of Texture2D files as PNG images.
|
|
3
|
+
|
|
4
|
+
## Upgrade Notice
|
|
5
|
+
### From 2.x / no version number to 3.x+
|
|
6
|
+
When upgrading from versions 2.x or with no version number, the project has to be newly set up. To retain all current data the following folders can be copied to the new folder of the project:
|
|
7
|
+
- `config`: Only user_config.yml is required, the rest can be deleted.
|
|
8
|
+
- `ClientAssets` or directory set in `asset-directory` of the config: Contains all currently downloaded assets, version information und update logs used for extraction. Highly recommend to transfer to the new project folder.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
Before installation, Python 3.11 or newer needs to be available on the system. It is recommended to set the project up using [venv](https://docs.python.org/3/tutorial/venv.html) or a similar virtual environment manager. The project can be installed using pip:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
pip install azlassets
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Alternatively, to install the newest version from the repository (requires git on the system):
|
|
18
|
+
```
|
|
19
|
+
pip install git+https://github.com/nobbyfix/AzurLane-AssetDownloader.git
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
There are three scripts to manage the assets:
|
|
24
|
+
- `obb_apk_import`: Importing assets from obb/apk/xapk files
|
|
25
|
+
- `downlader`: Downloading assets from the game server
|
|
26
|
+
- `extractor`: Extract PNGs from the assets
|
|
27
|
+
|
|
28
|
+
These can be executed using `py -m <scriptname>` on Windows or `python3 -m <scriptname>` on Linux/macOS (will be shortened to `py[thon3]` going forward, use the appropriate version for your system). Detailed usage will be explained in the following sections.
|
|
29
|
+
|
|
30
|
+
### 1. Import files from xapk/apk/obb
|
|
31
|
+
While this is *not necessary*, this step is **recommended** if you want all game assets available and not spam the game update server with errors of missing files on the first download.
|
|
32
|
+
|
|
33
|
+
The `obb_apk_import.py` supports all game clients (EN, JP, CN, KR, TW) and multiple forms of importing the assets. The recommended and easiest way is by downloading the `.xapk` from one of many Google Play Store app distributors (like APKMirror or APKPure). You can find them by searching for the package name, which are as follows:
|
|
34
|
+
- EN: com.YoStarEN.AzurLane
|
|
35
|
+
- JP: com.YoStarJP.AzurLane
|
|
36
|
+
- KR: kr.txwy.and.blhx
|
|
37
|
+
- TW: com.hkmanjuu.azurlane.gp
|
|
38
|
+
|
|
39
|
+
Alternatively if you already have the game installed, for example on emulators, you can copy the obb file onto your system and use it instead of the xapk. On Android it can be found in the folder `/storage/emulated/0/Android/obb/[PACKAGE_NAME]/`.
|
|
40
|
+
|
|
41
|
+
Since the CN client is not distributed through the Google Play Store, there is no xapk/obb file for it, but you can find the android download link on the [website](https://game.bilibili.com/blhx/) which will download an apk file (not xapk like the others). Alternatively, the APK is installed in the folder `/data/app/com.bilibili.azurlane-1/` on android (Note: Root access is required to access this folder).
|
|
42
|
+
|
|
43
|
+
You can then execute the script by passing it the filepath to the xapk/apk/obb:
|
|
44
|
+
```
|
|
45
|
+
py[thon3] -m obb_apk_import [FILEPATH]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Settings
|
|
49
|
+
The `config/user_config.yml` file provides a few settings to filter which files will be downloaded (and later also extracted). The options `download-folder-listtype` and `extract-folder-listtype` can be set to either "blacklist" or "whitelist". Depending on this it will filter by the top-level folder names (subfolders are not supported) or top-level filenames (files inside top-level folders or lower cannot be filtered) set in `download-folder-list` and `extract-folder-list`. This allows to cut down the download and extraction times by skipping unneeded assets.
|
|
50
|
+
|
|
51
|
+
### 3. Download new updates from the game
|
|
52
|
+
All assets normally distributed via the in-app downloader can be downloaded by simply executing:
|
|
53
|
+
```
|
|
54
|
+
py[thon3] -m downloader [CLIENT]
|
|
55
|
+
```
|
|
56
|
+
where `CLIENT` has to be either EN, CN, JP, KR or TW. You can check which files have been downloaded or deleted using the difflog files in `ClientAssets/[CLIENT]/difflog`.
|
|
57
|
+
|
|
58
|
+
### 4. Extract all new and changed files
|
|
59
|
+
The asset extraction script supports extraction of all newly downloaded files and single asset bundles. The newly downloaded assets can be extracted by executing:
|
|
60
|
+
```
|
|
61
|
+
py[thon3] -m extractor [CLIENT]
|
|
62
|
+
```
|
|
63
|
+
where `CLIENT` is again one of EN, CN, JP, KR or TW. The extracted images will then be saved in `ClientExtract/[CLIENT]/` Since only Texture2D assets are exported, it's not desired to try to export from all assetbundles (See [settings section](#2-settings)).
|
|
64
|
+
|
|
65
|
+
A single assetbundle can be extracted by passing the filepath to the script:
|
|
66
|
+
```
|
|
67
|
+
py[thon3] -m extractor -f [FILEPATH]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 5. Enjoy the files
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "azlassets"
|
|
7
|
+
authors = [{ name = "nobbyfix" }]
|
|
8
|
+
description ="Azur Lane asset downloader and extractor"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
dynamic = ["version"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"UnityPy >= 1.10.18",
|
|
15
|
+
"PyYAML",
|
|
16
|
+
"Pillow",
|
|
17
|
+
"protobuf",
|
|
18
|
+
"aiofile",
|
|
19
|
+
"aiohttp[speedups]"
|
|
20
|
+
]
|
|
21
|
+
keywords = [
|
|
22
|
+
"azurlane",
|
|
23
|
+
"azur-lane"
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 5 - Production/Stable",
|
|
27
|
+
"Environment :: Console",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"Intended Audience :: End Users/Desktop",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
"Programming Language :: Python",
|
|
32
|
+
"Programming Language :: Python :: 3",
|
|
33
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
34
|
+
"Programming Language :: Python :: 3.11",
|
|
35
|
+
"Programming Language :: Python :: 3.12",
|
|
36
|
+
"Programming Language :: Python :: 3.13",
|
|
37
|
+
"Programming Language :: Python :: 3.14",
|
|
38
|
+
"Programming Language :: Python :: 3.15",
|
|
39
|
+
"Topic :: Games/Entertainment"
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
dev = ["pre-commit"]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
homepage = "https://github.com/nobbyfix/AzurLane-AssetDownloader"
|
|
47
|
+
repository = "https://github.com/nobbyfix/AzurLane-AssetDownloader.git"
|
|
48
|
+
issues = "https://github.com/nobbyfix/AzurLane-AssetDownloader/issues"
|
|
49
|
+
changelog = "https://github.com/nobbyfix/AzurLane-AssetDownloader/blob/master/CHANGELOG.md"
|
|
50
|
+
download = "https://github.com/nobbyfix/AzurLane-AssetDownloader/releases/latest"
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.dynamic]
|
|
53
|
+
version = { attr = "azlassets.__version__" }
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.package-data]
|
|
56
|
+
"azlassets.config" = ["*.json", "*.yml"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.3.0" # x-release-please-version
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
CompareType = Enum('CompareType', 'New Changed Unchanged Deleted')
|
|
8
|
+
DownloadType = Enum('DownloadType', 'NoChange Removed Success Failed ForDeletionNoChange')
|
|
9
|
+
|
|
10
|
+
class VersionType(Enum):
|
|
11
|
+
__hash2member_map__: dict[str, Self] = {}
|
|
12
|
+
hashname: str
|
|
13
|
+
"""Hash name used on the version result returned by the game server."""
|
|
14
|
+
suffix: str
|
|
15
|
+
"""Suffix used on version and hash files."""
|
|
16
|
+
|
|
17
|
+
AZL = (1, "azhash", "")
|
|
18
|
+
CV = (2, "cvhash", "cv")
|
|
19
|
+
L2D = (3, "l2dhash", "live2d")
|
|
20
|
+
PIC = (4, "pichash", "pic")
|
|
21
|
+
BGM = (5, "bgmhash", "bgm")
|
|
22
|
+
CIPHER = (6, "cipherhash", "cipher")
|
|
23
|
+
MANGA = (7, "mangahash", "manga")
|
|
24
|
+
PAINTING = (8, "paintinghash", "painting")
|
|
25
|
+
DORM = (9, "dormhash", "dorm")
|
|
26
|
+
MAP = (10, "maphash", "map")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def __init__(self, _, hashname, suffix) -> None:
|
|
30
|
+
# add attributes to enum objects
|
|
31
|
+
self.hashname = hashname
|
|
32
|
+
self.suffix = suffix
|
|
33
|
+
# add enum objects to member maps
|
|
34
|
+
self.__hash2member_map__[hashname] = self
|
|
35
|
+
|
|
36
|
+
def __str__(self) -> str:
|
|
37
|
+
return self.name.lower()
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def version_filename(self) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Full version filename using the suffix.
|
|
43
|
+
"""
|
|
44
|
+
suffix = self.suffix
|
|
45
|
+
if suffix: suffix = "-"+suffix
|
|
46
|
+
return f"version{suffix}.txt"
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def hashes_filename(self) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Full hashes filename using the suffix.
|
|
52
|
+
"""
|
|
53
|
+
suffix = self.suffix
|
|
54
|
+
if suffix: suffix = "-"+suffix
|
|
55
|
+
return f"hashes{suffix}.csv"
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_hashname(cls, hashname: str) -> Self | None:
|
|
59
|
+
"""
|
|
60
|
+
Returns a VersionType member with matching *hashname* if match exists, otherwise None.
|
|
61
|
+
"""
|
|
62
|
+
return cls.__hash2member_map__.get(hashname)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AbstractClient(Enum):
|
|
66
|
+
active: bool
|
|
67
|
+
locale_code: str
|
|
68
|
+
package_name: str
|
|
69
|
+
|
|
70
|
+
def __new__(cls, value, active, locale, package_name):
|
|
71
|
+
# this should be done differently, but i am too lazy to do that now
|
|
72
|
+
# TODO: change it
|
|
73
|
+
if not hasattr(cls, "package_names"):
|
|
74
|
+
cls.package_names = {}
|
|
75
|
+
|
|
76
|
+
obj = object.__new__(cls)
|
|
77
|
+
obj._value_ = value
|
|
78
|
+
obj.active = active
|
|
79
|
+
obj.locale_code = locale
|
|
80
|
+
obj.package_name = package_name
|
|
81
|
+
cls.package_names[package_name] = obj
|
|
82
|
+
return obj
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def from_package_name(cls, package_name) -> Self | None:
|
|
86
|
+
return cls.package_names.get(package_name)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Client(AbstractClient):
|
|
90
|
+
EN = (1, True, 'en-US', 'com.YoStarEN.AzurLane')
|
|
91
|
+
JP = (2, True, 'ja-JP', 'com.YoStarJP.AzurLane')
|
|
92
|
+
CN = (3, True, 'zh-CN', '')
|
|
93
|
+
KR = (4, True, 'ko-KR', 'kr.txwy.and.blhx')
|
|
94
|
+
TW = (5, True, 'zh-TW', 'com.hkmanjuu.azurlane.gp')
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class HashRow:
|
|
99
|
+
filepath: str
|
|
100
|
+
size: int
|
|
101
|
+
md5hash: str
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class CompareResult:
|
|
105
|
+
current_hash: HashRow | None
|
|
106
|
+
new_hash: HashRow | None
|
|
107
|
+
compare_type: CompareType
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class VersionResult:
|
|
111
|
+
version: str
|
|
112
|
+
vhash: str
|
|
113
|
+
rawstring: str
|
|
114
|
+
version_type: VersionType
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class BundlePath:
|
|
118
|
+
full: Path
|
|
119
|
+
inner: str
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def construct(parentdir: Path, inner: Path | str) -> "BundlePath":
|
|
123
|
+
fullpath = Path(parentdir, inner)
|
|
124
|
+
return BundlePath(fullpath, str(inner))
|
|
125
|
+
|
|
126
|
+
def __hash__(self):
|
|
127
|
+
return hash(self.inner)
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class UpdateResult:
|
|
131
|
+
compare_result: CompareResult
|
|
132
|
+
download_type: DownloadType
|
|
133
|
+
path: BundlePath
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class UserConfig:
|
|
137
|
+
useragent: str
|
|
138
|
+
download_isblacklist: bool
|
|
139
|
+
download_filter: list
|
|
140
|
+
extract_isblacklist: bool
|
|
141
|
+
extract_filter: list
|
|
142
|
+
asset_directory: Path
|
|
143
|
+
extract_directory: Path
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class ClientConfig:
|
|
147
|
+
gateip: str
|
|
148
|
+
gateport: int
|
|
149
|
+
cdnurl: str
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# stolen from https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console
|
|
153
|
+
def printProgressBar(iteration, total, prefix = '', suffix = 'Complete', decimals = 1, length = 50, fill = '█', printEnd = "\r", details_unit = None):
|
|
154
|
+
"""
|
|
155
|
+
Call in a loop to create terminal progress bar
|
|
156
|
+
@params:
|
|
157
|
+
iteration - Required : current iteration (Int)
|
|
158
|
+
total - Required : total iterations (Int)
|
|
159
|
+
prefix - Optional : prefix string (Str)
|
|
160
|
+
suffix - Optional : suffix string (Str)
|
|
161
|
+
decimals - Optional : positive number of decimals in percent complete (Int)
|
|
162
|
+
length - Optional : character length of bar (Int)
|
|
163
|
+
fill - Optional : bar fill character (Str)
|
|
164
|
+
printEnd - Optional : end character (e.g. "\r", "\r\n") (Str)
|
|
165
|
+
"""
|
|
166
|
+
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
|
|
167
|
+
filledLength = int(length * iteration // total)
|
|
168
|
+
progress = fill * filledLength + '-' * (length - filledLength)
|
|
169
|
+
details = f" [{iteration}/{total} {details_unit}]" if details_unit else ""
|
|
170
|
+
print(f'\r{prefix} |{progress}| {percent}% {suffix}{details}', end = printEnd)
|
|
171
|
+
# Print New Line on Complete
|
|
172
|
+
if iteration == total:
|
|
173
|
+
print()
|
|
174
|
+
|
|
175
|
+
# simplified progress bar class with only the useful stuff i use
|
|
176
|
+
class ProgressBar():
|
|
177
|
+
def __init__(self, total: int, prefix: str, suffix: str = "Complete", iterstart: int = 0, details_unit: str | None = None, print_on_init: bool = True):
|
|
178
|
+
self.iteration = iterstart
|
|
179
|
+
self.total = total
|
|
180
|
+
self.prefix = prefix
|
|
181
|
+
self.suffix = suffix
|
|
182
|
+
self.details_unit = details_unit
|
|
183
|
+
if print_on_init:
|
|
184
|
+
printProgressBar(iterstart, total, prefix, suffix, details_unit=details_unit)
|
|
185
|
+
|
|
186
|
+
def update(self, iteration: int | None = None):
|
|
187
|
+
if iteration:
|
|
188
|
+
self.iteration = iteration
|
|
189
|
+
else:
|
|
190
|
+
self.iteration += 1
|
|
191
|
+
printProgressBar(self.iteration, self.total, self.prefix, self.suffix, details_unit=self.details_unit)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"EN": {
|
|
3
|
+
"gateip": "blhxusgate.yo-star.com",
|
|
4
|
+
"gateport": 80,
|
|
5
|
+
"cdnurl": "https://blhxusstatic.yo-star.com"
|
|
6
|
+
},
|
|
7
|
+
"CN": {
|
|
8
|
+
"gateip": "line1-login-bili-blhx.bilibiligame.net",
|
|
9
|
+
"gateport": 80,
|
|
10
|
+
"cdnurl": "https://line3-patch-blhx.bilibiligame.net"
|
|
11
|
+
},
|
|
12
|
+
"JP": {
|
|
13
|
+
"gateip": "blhxjploginapi.azurlane.jp",
|
|
14
|
+
"gateport": 80,
|
|
15
|
+
"cdnurl": "https://blhxstatic.yo-star.com"
|
|
16
|
+
},
|
|
17
|
+
"KR": {
|
|
18
|
+
"gateip": "bl-kr-gate.xdg.com",
|
|
19
|
+
"gateport": 80,
|
|
20
|
+
"cdnurl": "http://blcdn.imtxwy.com"
|
|
21
|
+
},
|
|
22
|
+
"TW": {
|
|
23
|
+
"gateip": "prod-all-login.azurlane.tw",
|
|
24
|
+
"gateport": 10080,
|
|
25
|
+
"cdnurl": "http://blhxstatic.azurlane.tw"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
asset-directory: ClientAssets
|
|
2
|
+
extract-directory: ClientExtract
|
|
3
|
+
# set to blacklist or whitelist
|
|
4
|
+
download-folder-listtype: blacklist
|
|
5
|
+
extract-folder-listtype: whitelist
|
|
6
|
+
download-folder-list: []
|
|
7
|
+
extract-folder-list:
|
|
8
|
+
- activitybanner
|
|
9
|
+
- aircrafticon
|
|
10
|
+
- backyardtheme
|
|
11
|
+
- battlescore
|
|
12
|
+
- bg
|
|
13
|
+
- boxprefab
|
|
14
|
+
- chargeicon
|
|
15
|
+
- clutter
|
|
16
|
+
- collectionfileillustration
|
|
17
|
+
- collectionfiletitle
|
|
18
|
+
- commanderhrz
|
|
19
|
+
- commandericon
|
|
20
|
+
- commanderskillicon
|
|
21
|
+
- commandertalenticon
|
|
22
|
+
- commonbg
|
|
23
|
+
- crusingwindow
|
|
24
|
+
- emblem
|
|
25
|
+
- enemies
|
|
26
|
+
- equips
|
|
27
|
+
- eventtype
|
|
28
|
+
- extra_page
|
|
29
|
+
- furnitureicon
|
|
30
|
+
- gallerycard
|
|
31
|
+
- guildnode
|
|
32
|
+
- guildboss
|
|
33
|
+
- guildevent
|
|
34
|
+
- guildmission
|
|
35
|
+
- helpbg
|
|
36
|
+
- herohrzicon
|
|
37
|
+
- icondesc
|
|
38
|
+
- iconframe
|
|
39
|
+
- independenttex
|
|
40
|
+
- levelmap
|
|
41
|
+
- loadingbg
|
|
42
|
+
- lotterybg
|
|
43
|
+
- mangapic
|
|
44
|
+
- medal
|
|
45
|
+
- memoryicon
|
|
46
|
+
- metaship
|
|
47
|
+
- musiccover
|
|
48
|
+
- newshipbg
|
|
49
|
+
- newyearskinshowpage
|
|
50
|
+
- painting
|
|
51
|
+
- prints
|
|
52
|
+
- props
|
|
53
|
+
- qicon
|
|
54
|
+
- shipmodels
|
|
55
|
+
- shiprarity
|
|
56
|
+
- shipyardicon
|
|
57
|
+
- skillicon
|
|
58
|
+
- squareicon
|
|
59
|
+
- strategyicon
|
|
60
|
+
- updatebg
|
|
61
|
+
useragent: ''
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json
|
|
3
|
+
import yaml
|
|
4
|
+
from shutil import copy
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from importlib.resources import files
|
|
7
|
+
|
|
8
|
+
from .classes import Client, UserConfig, ClientConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# package-incuded filepaths
|
|
12
|
+
CONFIG_DATA_PATH = files("azlassets").joinpath("config")
|
|
13
|
+
YAML_TEMPLATE_PATH = CONFIG_DATA_PATH.joinpath("user_config_template.yml")
|
|
14
|
+
CLIENT_CONFIG_PATH = CONFIG_DATA_PATH.joinpath("client_config.json")
|
|
15
|
+
|
|
16
|
+
# cwd-relative filepaths
|
|
17
|
+
YAML_CONFIG_PATH = Path("config") / "user_config.yml"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_user_config() -> UserConfig:
|
|
21
|
+
if not YAML_CONFIG_PATH.exists():
|
|
22
|
+
print("Userconfig does not exist. A new one will be created.")
|
|
23
|
+
print("Note that the useragent is empty and it is advised to set one.")
|
|
24
|
+
YAML_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
copy(YAML_TEMPLATE_PATH, YAML_CONFIG_PATH)
|
|
26
|
+
return load_user_config()
|
|
27
|
+
|
|
28
|
+
with open(YAML_CONFIG_PATH, 'r', encoding='utf8') as file:
|
|
29
|
+
yamlconfig = yaml.safe_load(file)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
userconfig = UserConfig(
|
|
33
|
+
useragent=yamlconfig['useragent'],
|
|
34
|
+
download_isblacklist=yamlconfig['download-folder-listtype'] == 'blacklist',
|
|
35
|
+
download_filter=yamlconfig['download-folder-list'],
|
|
36
|
+
extract_isblacklist=yamlconfig['extract-folder-listtype'] == 'blacklist',
|
|
37
|
+
extract_filter=yamlconfig['extract-folder-list'],
|
|
38
|
+
asset_directory=yamlconfig['asset-directory'],
|
|
39
|
+
extract_directory=yamlconfig['extract-directory'],
|
|
40
|
+
)
|
|
41
|
+
except KeyError:
|
|
42
|
+
print("There is an error inside the userconfig file. Delete it or change the wrong values.")
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
return userconfig
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_client_config(client: Client) -> ClientConfig:
|
|
49
|
+
with open(CLIENT_CONFIG_PATH, 'r', encoding='utf8') as f:
|
|
50
|
+
configdata = json.load(f)
|
|
51
|
+
|
|
52
|
+
if not client.name in configdata:
|
|
53
|
+
raise NotImplementedError(f'Client {client.name} has not been configured yet.')
|
|
54
|
+
|
|
55
|
+
config = configdata[client.name]
|
|
56
|
+
try:
|
|
57
|
+
clientconfig = ClientConfig(config['gateip'], config['gateport'], config['cdnurl'])
|
|
58
|
+
except KeyError:
|
|
59
|
+
print("The clientconfig has been wrongly configured.")
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
|
|
62
|
+
return clientconfig
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
import aiofile
|
|
3
|
+
import traceback
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .classes import VersionResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AzurlaneAsyncDownloader(aiohttp.ClientSession):
|
|
10
|
+
def __init__(self, cdn_url: str, useragent: str):
|
|
11
|
+
base_url = f"{cdn_url}/android/"
|
|
12
|
+
limited_tcp_connector = aiohttp.TCPConnector(limit_per_host=6)
|
|
13
|
+
super().__init__(base_url=base_url, headers={"user-agent": useragent}, connector=limited_tcp_connector)
|
|
14
|
+
|
|
15
|
+
async def get_hashes(self, versionhash: str) -> aiohttp.ClientResponse:
|
|
16
|
+
return await self.get(f"hash/{versionhash}")
|
|
17
|
+
|
|
18
|
+
async def get_asset(self, filehash: str) -> aiohttp.ClientResponse:
|
|
19
|
+
return await self.get(f"resource/{filehash}")
|
|
20
|
+
|
|
21
|
+
async def download_hashes(self, version_result: VersionResult) -> str | None:
|
|
22
|
+
try:
|
|
23
|
+
async with await self.get_hashes(version_result.rawstring) as response:
|
|
24
|
+
response: aiohttp.ClientResponse
|
|
25
|
+
response.raise_for_status() # raises error on bad HTTP status
|
|
26
|
+
|
|
27
|
+
hashes = await response.text()
|
|
28
|
+
return hashes
|
|
29
|
+
|
|
30
|
+
except Exception as e:
|
|
31
|
+
print(f"ERROR: An unexpected error occured while downloading '{version_result.version_type.name}' hashfile.")
|
|
32
|
+
traceback.print_exception(type(e), e, e.__traceback__)
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
async def download_asset(self, filehash: str, save_destination: Path, expected_file_size: int) -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Downloads the requested file using the session and saves it to 'save_destination' on disk.
|
|
38
|
+
|
|
39
|
+
Returns `True` if the operation was successful, otherwise `False`.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
async with await self.get_asset(filehash) as response:
|
|
43
|
+
response: aiohttp.ClientResponse
|
|
44
|
+
response.raise_for_status() # raises error on bad HTTP status
|
|
45
|
+
|
|
46
|
+
# reject response if response size doesn't match expected size
|
|
47
|
+
response_size = response.content_length
|
|
48
|
+
if expected_file_size != response_size:
|
|
49
|
+
print(f"ERROR: Received asset '{filehash}' with target '{save_destination}' has wrong size ({response_size}/{expected_file_size}).")
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
save_destination.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
async with aiofile.async_open(save_destination, "wb") as file:
|
|
54
|
+
async for chunk in response.content.iter_chunked(1024*16): # no idea what chuck size is best
|
|
55
|
+
await file.write(chunk)
|
|
56
|
+
|
|
57
|
+
return True
|
|
58
|
+
except Exception as e:
|
|
59
|
+
print(f"ERROR: An unexpected error occured while downloading '{filehash}' to '{save_destination}'.")
|
|
60
|
+
traceback.print_exception(type(e), e, e.__traceback__)
|
|
61
|
+
return False
|