Compare commits
15 Commits
Author | SHA1 | Date |
---|---|---|
tretrauit | 0140969e4b | |
tretrauit | 680cf60720 | |
tretrauit | 6a76c49323 | |
tretrauit | 66163f8f45 | |
tretrauit | 208b34c9df | |
tretrauit | f61e4a39cf | |
tretrauit | e133508d39 | |
tretrauit | 96d1c7e8d3 | |
tretrauit | 811403bdfa | |
tretrauit | 44f0d10cb8 | |
tretrauit | 2abbdf066b | |
tretrauit | f909297ae0 | |
tretrauit | 42d7179ea5 | |
tretrauit | c8823ea3b1 | |
tretrauit | 7d4f4e7931 |
|
@ -2,8 +2,4 @@
|
|||
|
||||
A worthless CLI launcher written in Python.
|
||||
|
||||
> For a nice GUI launcher you should check out [An Anime Game Launcher](https://github.com/an-anime-team/an-anime-game-launcher)
|
||||
|
||||
Check out its website at https://tretrauit.gitlab.io/worthless-launcher for more information.
|
||||
|
||||
> The current branch will enter maintenance mode, for the latest development please check `refactor` branch
|
||||
|
|
|
@ -0,0 +1,786 @@
|
|||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.8.1"
|
||||
description = "Async http client/server framework (asyncio)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
aiosignal = ">=1.1.2"
|
||||
async-timeout = ">=4.0.0a3,<5.0"
|
||||
attrs = ">=17.3.0"
|
||||
charset-normalizer = ">=2.0,<3.0"
|
||||
frozenlist = ">=1.1.1"
|
||||
multidict = ">=4.5,<7.0"
|
||||
yarl = ">=1.0,<2.0"
|
||||
|
||||
[package.extras]
|
||||
speedups = ["Brotli", "aiodns", "cchardet"]
|
||||
|
||||
[[package]]
|
||||
name = "aiosignal"
|
||||
version = "1.2.0"
|
||||
description = "aiosignal: a list of registered asynchronous callbacks"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
frozenlist = ">=1.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "appdirs"
|
||||
version = "1.4.4"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "4.0.2"
|
||||
description = "Timeout context manager for asyncio programs"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "22.1.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.extras]
|
||||
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
|
||||
tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
|
||||
|
||||
[[package]]
|
||||
name = "bitstruct"
|
||||
version = "8.15.1"
|
||||
description = "This module performs conversions between Python values and C bit field structs represented as Python byte strings."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.15.1"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
pycparser = "*"
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "2.1.1"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
|
||||
[package.extras]
|
||||
unicode_backport = ["unicodedata2"]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.5"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "detools"
|
||||
version = "0.51.0"
|
||||
description = "Binary delta encoding tools."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
bitstruct = "*"
|
||||
heatshrink2 = "*"
|
||||
humanfriendly = "*"
|
||||
lz4 = "*"
|
||||
pyelftools = "*"
|
||||
zstandard = "*"
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.3.1"
|
||||
description = "A list-like structure which implements collections.abc.MutableSequence"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "heatshrink2"
|
||||
version = "0.11.0"
|
||||
description = "Python bindings to the heatshrink library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "humanfriendly"
|
||||
version = "10.0"
|
||||
description = "Human friendly output for text interfaces using Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.dependencies]
|
||||
pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""}
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.4"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "1.1.1"
|
||||
description = "iniconfig: brain-dead simple config-ini parsing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "lz4"
|
||||
version = "4.0.2"
|
||||
description = "LZ4 Bindings for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=1.6.0)", "sphinx-bootstrap-theme"]
|
||||
flake8 = ["flake8"]
|
||||
tests = ["psutil", "pytest (!=3.3.0)", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.0.2"
|
||||
description = "multidict implementation"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "21.3"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.0.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "py"
|
||||
version = "1.11.0"
|
||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.21"
|
||||
description = "C parser in Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pyelftools"
|
||||
version = "0.29"
|
||||
description = "Library for analyzing ELF files and DWARF debugging information"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.0.9"
|
||||
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.8"
|
||||
|
||||
[package.extras]
|
||||
diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[[package]]
|
||||
name = "pyreadline3"
|
||||
version = "3.4.1"
|
||||
description = "A python implementation of GNU readline."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.1.3"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
attrs = ">=19.2.0"
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
py = ">=1.8.2"
|
||||
tomli = ">=1.0.0"
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
description = "A lil' TOML parser"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.8.1"
|
||||
description = "Yet another URL library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
idna = ">=2.0"
|
||||
multidict = ">=4.0"
|
||||
|
||||
[[package]]
|
||||
name = "zstandard"
|
||||
version = "0.18.0"
|
||||
description = "Zstandard bindings for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\""}
|
||||
|
||||
[package.extras]
|
||||
cffi = ["cffi (>=1.11)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "6be8320905e4c9f9da4ae9d4d3bd2066bd2d5a475851030bd7f2c362aaefeed2"
|
||||
|
||||
[metadata.files]
|
||||
aiohttp = [
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"},
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"},
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"},
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"},
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"},
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"},
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"},
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"},
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"},
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"},
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"},
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"},
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"},
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"},
|
||||
{file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"},
|
||||
{file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"},
|
||||
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"},
|
||||
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"},
|
||||
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"},
|
||||
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"},
|
||||
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"},
|
||||
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"},
|
||||
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"},
|
||||
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"},
|
||||
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"},
|
||||
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"},
|
||||
{file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"},
|
||||
{file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"},
|
||||
{file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"},
|
||||
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"},
|
||||
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"},
|
||||
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"},
|
||||
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"},
|
||||
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"},
|
||||
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"},
|
||||
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"},
|
||||
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"},
|
||||
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"},
|
||||
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"},
|
||||
{file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"},
|
||||
{file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"},
|
||||
{file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"},
|
||||
{file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"},
|
||||
{file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"},
|
||||
]
|
||||
aiosignal = [
|
||||
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
|
||||
{file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
|
||||
]
|
||||
appdirs = [
|
||||
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
|
||||
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
|
||||
]
|
||||
async-timeout = [
|
||||
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
|
||||
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
|
||||
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
|
||||
]
|
||||
bitstruct = [
|
||||
{file = "bitstruct-8.15.1.tar.gz", hash = "sha256:6fa6adbfb8f3b8cb68c21b13aa65d23eb2c3ac32419ab926f3fd1fff717a9125"},
|
||||
]
|
||||
cffi = [
|
||||
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
|
||||
{file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
|
||||
{file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
|
||||
{file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
|
||||
{file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
|
||||
{file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
|
||||
{file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
|
||||
{file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
|
||||
{file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
|
||||
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
|
||||
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
|
||||
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
|
||||
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
|
||||
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
|
||||
{file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
|
||||
{file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
|
||||
{file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
|
||||
{file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
|
||||
{file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
|
||||
{file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
|
||||
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
|
||||
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
|
||||
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
|
||||
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
|
||||
{file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
|
||||
{file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
|
||||
{file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
|
||||
{file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
|
||||
{file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
|
||||
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
|
||||
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
|
||||
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
|
||||
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
|
||||
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
|
||||
{file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
|
||||
{file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
|
||||
{file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
|
||||
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
|
||||
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
|
||||
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
|
||||
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
|
||||
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
|
||||
{file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
|
||||
{file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
|
||||
{file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
|
||||
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
|
||||
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
|
||||
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
|
||||
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
|
||||
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
|
||||
{file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
|
||||
{file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
|
||||
{file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
|
||||
{file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
|
||||
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
|
||||
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
|
||||
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
|
||||
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
|
||||
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
|
||||
{file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
|
||||
{file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
|
||||
{file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
|
||||
{file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
|
||||
{file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
|
||||
]
|
||||
charset-normalizer = [
|
||||
{file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
|
||||
{file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
||||
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
||||
]
|
||||
detools = [
|
||||
{file = "detools-0.51.0.tar.gz", hash = "sha256:f53d72a9a036c52429910ca0aa7f84ef22fa758231821b2240d2021c50e822b5"},
|
||||
]
|
||||
frozenlist = [
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f271c93f001748fc26ddea409241312a75e13466b06c94798d1a341cf0e6989"},
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c6ef8014b842f01f5d2b55315f1af5cbfde284eb184075c189fd657c2fd8204"},
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:219a9676e2eae91cb5cc695a78b4cb43d8123e4160441d2b6ce8d2c70c60e2f3"},
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b47d64cdd973aede3dd71a9364742c542587db214e63b7529fbb487ed67cddd9"},
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2af6f7a4e93f5d08ee3f9152bce41a6015b5cf87546cb63872cc19b45476e98a"},
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a718b427ff781c4f4e975525edb092ee2cdef6a9e7bc49e15063b088961806f8"},
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c56c299602c70bc1bb5d1e75f7d8c007ca40c9d7aebaf6e4ba52925d88ef826d"},
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717470bfafbb9d9be624da7780c4296aa7935294bd43a075139c3d55659038ca"},
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:31b44f1feb3630146cffe56344704b730c33e042ffc78d21f2125a6a91168131"},
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c3b31180b82c519b8926e629bf9f19952c743e089c41380ddca5db556817b221"},
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d82bed73544e91fb081ab93e3725e45dd8515c675c0e9926b4e1f420a93a6ab9"},
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49459f193324fbd6413e8e03bd65789e5198a9fa3095e03f3620dee2f2dabff2"},
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:94e680aeedc7fd3b892b6fa8395b7b7cc4b344046c065ed4e7a1e390084e8cb5"},
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-win32.whl", hash = "sha256:fabb953ab913dadc1ff9dcc3a7a7d3dc6a92efab3a0373989b8063347f8705be"},
|
||||
{file = "frozenlist-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:eee0c5ecb58296580fc495ac99b003f64f82a74f9576a244d04978a7e97166db"},
|
||||
{file = "frozenlist-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bc75692fb3770cf2b5856a6c2c9de967ca744863c5e89595df64e252e4b3944"},
|
||||
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086ca1ac0a40e722d6833d4ce74f5bf1aba2c77cbfdc0cd83722ffea6da52a04"},
|
||||
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b51eb355e7f813bcda00276b0114c4172872dc5fb30e3fea059b9367c18fbcb"},
|
||||
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74140933d45271c1a1283f708c35187f94e1256079b3c43f0c2267f9db5845ff"},
|
||||
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee4c5120ddf7d4dd1eaf079af3af7102b56d919fa13ad55600a4e0ebe532779b"},
|
||||
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d9e00f3ac7c18e685320601f91468ec06c58acc185d18bb8e511f196c8d4b2"},
|
||||
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e19add867cebfb249b4e7beac382d33215d6d54476bb6be46b01f8cafb4878b"},
|
||||
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a027f8f723d07c3f21963caa7d585dcc9b089335565dabe9c814b5f70c52705a"},
|
||||
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:61d7857950a3139bce035ad0b0945f839532987dfb4c06cfe160254f4d19df03"},
|
||||
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:53b2b45052e7149ee8b96067793db8ecc1ae1111f2f96fe1f88ea5ad5fd92d10"},
|
||||
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bbb1a71b1784e68870800b1bc9f3313918edc63dbb8f29fbd2e767ce5821696c"},
|
||||
{file = "frozenlist-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:ab6fa8c7871877810e1b4e9392c187a60611fbf0226a9e0b11b7b92f5ac72792"},
|
||||
{file = "frozenlist-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89139662cc4e65a4813f4babb9ca9544e42bddb823d2ec434e18dad582543bc"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4c0c99e31491a1d92cde8648f2e7ccad0e9abb181f6ac3ddb9fc48b63301808e"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61e8cb51fba9f1f33887e22488bad1e28dd8325b72425f04517a4d285a04c519"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc2f3e368ee5242a2cbe28323a866656006382872c40869b49b265add546703f"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58fb94a01414cddcdc6839807db77ae8057d02ddafc94a42faee6004e46c9ba8"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:572ce381e9fe027ad5e055f143763637dcbac2542cfe27f1d688846baeef5170"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19127f8dcbc157ccb14c30e6f00392f372ddb64a6ffa7106b26ff2196477ee9f"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42719a8bd3792744c9b523674b752091a7962d0d2d117f0b417a3eba97d1164b"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2743bb63095ef306041c8f8ea22bd6e4d91adabf41887b1ad7886c4c1eb43d5f"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fa47319a10e0a076709644a0efbcaab9e91902c8bd8ef74c6adb19d320f69b83"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52137f0aea43e1993264a5180c467a08a3e372ca9d378244c2d86133f948b26b"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:f5abc8b4d0c5b556ed8cd41490b606fe99293175a82b98e652c3f2711b452988"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1e1cf7bc8cbbe6ce3881863671bac258b7d6bfc3706c600008925fb799a256e2"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-win32.whl", hash = "sha256:0dde791b9b97f189874d654c55c24bf7b6782343e14909c84beebd28b7217845"},
|
||||
{file = "frozenlist-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:9494122bf39da6422b0972c4579e248867b6b1b50c9b05df7e04a3f30b9a413d"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31bf9539284f39ff9398deabf5561c2b0da5bb475590b4e13dd8b268d7a3c5c1"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0c8c803f2f8db7217898d11657cb6042b9b0553a997c4a0601f48a691480fab"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da5ba7b59d954f1f214d352308d1d86994d713b13edd4b24a556bcc43d2ddbc3"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e6b2b456f21fc93ce1aff2b9728049f1464428ee2c9752a4b4f61e98c4db96"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526d5f20e954d103b1d47232e3839f3453c02077b74203e43407b962ab131e7b"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b499c6abe62a7a8d023e2c4b2834fce78a6115856ae95522f2f974139814538c"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab386503f53bbbc64d1ad4b6865bf001414930841a870fc97f1546d4d133f141"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f63c308f82a7954bf8263a6e6de0adc67c48a8b484fab18ff87f349af356efd"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:12607804084d2244a7bd4685c9d0dca5df17a6a926d4f1967aa7978b1028f89f"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:da1cdfa96425cbe51f8afa43e392366ed0b36ce398f08b60de6b97e3ed4affef"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f810e764617b0748b49a731ffaa525d9bb36ff38332411704c2400125af859a6"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:35c3d79b81908579beb1fb4e7fcd802b7b4921f1b66055af2578ff7734711cfa"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c92deb5d9acce226a501b77307b3b60b264ca21862bd7d3e0c1f3594022f01bc"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-win32.whl", hash = "sha256:5e77a8bd41e54b05e4fb2708dc6ce28ee70325f8c6f50f3df86a44ecb1d7a19b"},
|
||||
{file = "frozenlist-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:625d8472c67f2d96f9a4302a947f92a7adbc1e20bedb6aff8dbc8ff039ca6189"},
|
||||
{file = "frozenlist-1.3.1.tar.gz", hash = "sha256:3a735e4211a04ccfa3f4833547acdf5d2f863bfeb01cfd3edaffbc251f15cec8"},
|
||||
]
|
||||
heatshrink2 = [
|
||||
{file = "heatshrink2-0.11.0-cp310-cp310-win32.whl", hash = "sha256:afd697de31deab5742496864c286a005de099b10a574f013d353392814c3d954"},
|
||||
{file = "heatshrink2-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:04e14f5f51a70249d10463d2f6190c2443ddb06b0db46eb9eba50c3c349fbfb8"},
|
||||
{file = "heatshrink2-0.11.0-cp37-cp37m-win32.whl", hash = "sha256:bc744b7481be60e23eaa970adcc0a0a9ade9afa5f38cf0eb4c7756abd54e6049"},
|
||||
{file = "heatshrink2-0.11.0-cp37-cp37m-win_amd64.whl", hash = "sha256:42acd10f57fdab11d193d4e59fa2852d494621b8a09d1019e889592e209b86bb"},
|
||||
{file = "heatshrink2-0.11.0-cp38-cp38-win32.whl", hash = "sha256:d11c2ab1cafd591c64deda5587d311c9bfd6a43bf7e87cb7fd0a7582e8a31e14"},
|
||||
{file = "heatshrink2-0.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:c71cd0427a6c472be4fe425bf83f5677cc9e8341199ff4ea4f45b5968b66b8ee"},
|
||||
{file = "heatshrink2-0.11.0-cp39-cp39-win32.whl", hash = "sha256:f5d54c79a45982925fdc06b8906b2472441e4435564aa9db0f5b1d05a548efba"},
|
||||
{file = "heatshrink2-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:a9052fb9f6efadbb95630f4488f6abcf5f2c6169faa01df24a1277a6f2c50165"},
|
||||
{file = "heatshrink2-0.11.0.tar.gz", hash = "sha256:c6286d1d4b92fbaeb83b0fd8b5f831c9350dcd1046e776e5aaa83d1be43835f7"},
|
||||
]
|
||||
humanfriendly = [
|
||||
{file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"},
|
||||
{file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
]
|
||||
lz4 = [
|
||||
{file = "lz4-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3881573c3db902db370e072eb64b40c7c8289b94b2a731e051858cc198f890e8"},
|
||||
{file = "lz4-4.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:154e6e9f58a7bafc4d2a1395160305b78fc82fa708bfa58cf0ad977c443d1f8f"},
|
||||
{file = "lz4-4.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cfa82f26b4f1835c797bd70e5ce20d5f1ee897b9a0c53e62d607f9029f521ce"},
|
||||
{file = "lz4-4.0.2-cp310-cp310-win32.whl", hash = "sha256:fba1730cd2327a9d013192a9878714cc82f4877d2ada556222d03ea6428a80ed"},
|
||||
{file = "lz4-4.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:61dbcca64e8e1655e06b588356c4b2515bccc1d7e84065f858a685abd96f0cf2"},
|
||||
{file = "lz4-4.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56ea660097fec87f0c6746146b316775037f8dd886a4c5915360e5b32b7112d0"},
|
||||
{file = "lz4-4.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed86ab22bfe1f4cd4fc983704134a8fdf746c1121a398f8f14cbd014c1a5b0ae"},
|
||||
{file = "lz4-4.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:345608de23b4d68fbdef373f1e53d6c5abd99a062d4ff922e3350f47775ab123"},
|
||||
{file = "lz4-4.0.2-cp37-cp37m-win32.whl", hash = "sha256:5fe9db7627674875e4279c2ed50b1e38fb91ec3093347f871ed996e58edbb488"},
|
||||
{file = "lz4-4.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:3fa0f000d8ce39e643e9e5c49fc4d1985156ffb177e3123a0f22551f5864841b"},
|
||||
{file = "lz4-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6f3b3670f52f0871885258bcbc746f483760434336f0bc5581f161cc5d4b0c9a"},
|
||||
{file = "lz4-4.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea2c2182a5b0ad03f33ac09db0925a1738a1d65751a3e058110bd900c643d359"},
|
||||
{file = "lz4-4.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:439898dd4176a724243002003c3f733eb6ce48a5988175f54c8560e0b100b7a6"},
|
||||
{file = "lz4-4.0.2-cp38-cp38-win32.whl", hash = "sha256:35e6caced0229b90151d31d9cf1eaa541e597f8021bf5b70ff9e6374e3e43b23"},
|
||||
{file = "lz4-4.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1bd56282f6993e013ccf7f6edf1530c2a13d1662741e2be072349c7f70bc0682"},
|
||||
{file = "lz4-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1ed9a1875dc2a489f3b665d0211984689d0e76585e55650b044a64dbd2d22992"},
|
||||
{file = "lz4-4.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b18a6d6d9071c03dbf9e30bbe22e4476f24f1a4d73b1e975605ad3ce725e6c"},
|
||||
{file = "lz4-4.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d141719d3cbb7933809642a61b68b8f595ddf85657016521756ddcf826b85cd"},
|
||||
{file = "lz4-4.0.2-cp39-cp39-win32.whl", hash = "sha256:a8e02c2477bd704f43113ac8dd966c361187383591388818d74e1b73e4674759"},
|
||||
{file = "lz4-4.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:ee73357412c5505f6ba0ea61ff71455e2e4c1e04d8e60f17f3cd937261d773fa"},
|
||||
{file = "lz4-4.0.2.tar.gz", hash = "sha256:083b7172c2938412ae37c3a090250bfdd9e4a6e855442594f86c3608ed12729b"},
|
||||
]
|
||||
multidict = [
|
||||
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"},
|
||||
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"},
|
||||
{file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"},
|
||||
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"},
|
||||
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"},
|
||||
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"},
|
||||
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"},
|
||||
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"},
|
||||
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"},
|
||||
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"},
|
||||
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"},
|
||||
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"},
|
||||
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"},
|
||||
{file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"},
|
||||
{file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"},
|
||||
{file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"},
|
||||
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"},
|
||||
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"},
|
||||
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"},
|
||||
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"},
|
||||
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"},
|
||||
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"},
|
||||
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"},
|
||||
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"},
|
||||
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"},
|
||||
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"},
|
||||
{file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"},
|
||||
{file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"},
|
||||
{file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"},
|
||||
{file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"},
|
||||
{file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
|
||||
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
|
||||
]
|
||||
pluggy = [
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||
]
|
||||
py = [
|
||||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||
]
|
||||
pycparser = [
|
||||
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
||||
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||
]
|
||||
pyelftools = [
|
||||
{file = "pyelftools-0.29-py2.py3-none-any.whl", hash = "sha256:519f38cf412f073b2d7393aa4682b0190fa901f7c3fa0bff2b82d537690c7fc1"},
|
||||
{file = "pyelftools-0.29.tar.gz", hash = "sha256:ec761596aafa16e282a31de188737e5485552469ac63b60cfcccf22263fd24ff"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
||||
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
||||
]
|
||||
pyreadline3 = [
|
||||
{file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"},
|
||||
{file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
|
||||
{file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
|
||||
]
|
||||
tomli = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
yarl = [
|
||||
{file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"},
|
||||
{file = "yarl-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3"},
|
||||
{file = "yarl-1.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880"},
|
||||
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40"},
|
||||
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b"},
|
||||
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497"},
|
||||
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a"},
|
||||
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f"},
|
||||
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd"},
|
||||
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957"},
|
||||
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28"},
|
||||
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843"},
|
||||
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453"},
|
||||
{file = "yarl-1.8.1-cp310-cp310-win32.whl", hash = "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272"},
|
||||
{file = "yarl-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0"},
|
||||
{file = "yarl-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035"},
|
||||
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc"},
|
||||
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b"},
|
||||
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231"},
|
||||
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda"},
|
||||
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507"},
|
||||
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee"},
|
||||
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0"},
|
||||
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d"},
|
||||
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd"},
|
||||
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780"},
|
||||
{file = "yarl-1.8.1-cp37-cp37m-win32.whl", hash = "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07"},
|
||||
{file = "yarl-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-win32.whl", hash = "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0"},
|
||||
{file = "yarl-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-win32.whl", hash = "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6"},
|
||||
{file = "yarl-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be"},
|
||||
{file = "yarl-1.8.1.tar.gz", hash = "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf"},
|
||||
]
|
||||
zstandard = [
|
||||
{file = "zstandard-0.18.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef7e8a200e4c8ac9102ed3c90ed2aa379f6b880f63032200909c1be21951f556"},
|
||||
{file = "zstandard-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2dc466207016564805e56d28375f4f533b525ff50d6776946980dff5465566ac"},
|
||||
{file = "zstandard-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a2ee1d4f98447f3e5183ecfce5626f983504a4a0c005fbe92e60fa8e5d547ec"},
|
||||
{file = "zstandard-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d956e2f03c7200d7e61345e0880c292783ec26618d0d921dcad470cb195bbce2"},
|
||||
{file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ce6f59cba9854fd14da5bfe34217a1501143057313966637b7291d1b0267bd1e"},
|
||||
{file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fa67cba473623848b6e88acf8d799b1906178fd883fb3a1da24561c779593b"},
|
||||
{file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdb44d7284c8c5dd1b66dfb86dda7f4560fa94bfbbc1d2da749ba44831335e32"},
|
||||
{file = "zstandard-0.18.0-cp310-cp310-win32.whl", hash = "sha256:63694a376cde0aa8b1971d06ca28e8f8b5f492779cb6ee1cc46bbc3f019a42a5"},
|
||||
{file = "zstandard-0.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:702a8324cd90c74d9c8780d02bf55e79da3193c870c9665ad3a11647e3ad1435"},
|
||||
{file = "zstandard-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46f679bc5dfd938db4fb058218d9dc4db1336ffaf1ea774ff152ecadabd40805"},
|
||||
{file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc2a4de9f363b3247d472362a65041fe4c0f59e01a2846b15d13046be866a885"},
|
||||
{file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd3220d7627fd4d26397211cb3b560ec7cc4a94b75cfce89e847e8ce7fabe32d"},
|
||||
{file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:39e98cf4773234bd9cebf9f9db730e451dfcfe435e220f8921242afda8321887"},
|
||||
{file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5228e596eb1554598c872a337bbe4e5afe41cd1f8b1b15f2e35b50d061e35244"},
|
||||
{file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d4a8fd45746a6c31e729f35196e80b8f1e9987c59f5ccb8859d7c6a6fbeb9c63"},
|
||||
{file = "zstandard-0.18.0-cp36-cp36m-win32.whl", hash = "sha256:4cbb85f29a990c2fdbf7bc63246567061a362ddca886d7fae6f780267c0a9e67"},
|
||||
{file = "zstandard-0.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:bfa6c8549fa18e6497a738b7033c49f94a8e2e30c5fbe2d14d0b5aa8bbc1695d"},
|
||||
{file = "zstandard-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e02043297c1832f2666cd2204f381bef43b10d56929e13c42c10c732c6e3b4ed"},
|
||||
{file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7231543d38d2b7e02ef7cc78ef7ffd86419437e1114ff08709fe25a160e24bd6"},
|
||||
{file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c86befac87445927488f5c8f205d11566f64c11519db223e9d282b945fa60dab"},
|
||||
{file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999a4e1768f219826ba3fa2064fab1c86dd72fdd47a42536235478c3bb3ca3e2"},
|
||||
{file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df59cd1cf3c62075ee2a4da767089d19d874ac3ad42b04a71a167e91b384722"},
|
||||
{file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1be31e9e3f7607ee0cdd60915410a5968b205d3e7aa83b7fcf3dd76dbbdb39e0"},
|
||||
{file = "zstandard-0.18.0-cp37-cp37m-win32.whl", hash = "sha256:490d11b705b8ae9dc845431bacc8dd1cef2408aede176620a5cd0cd411027936"},
|
||||
{file = "zstandard-0.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:266aba27fa9cc5e9091d3d325ebab1fa260f64e83e42516d5e73947c70216a5b"},
|
||||
{file = "zstandard-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b2260c4e07dd0723eadb586de7718b61acca4083a490dda69c5719d79bc715c"},
|
||||
{file = "zstandard-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3af8c2383d02feb6650e9255491ec7d0824f6e6dd2bbe3e521c469c985f31fb1"},
|
||||
{file = "zstandard-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28723a1d2e4df778573b76b321ebe9f3469ac98988104c2af116dd344802c3f8"},
|
||||
{file = "zstandard-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19cac7108ff2c342317fad6dc97604b47a41f403c8f19d0bfc396dfadc3638b8"},
|
||||
{file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:76725d1ee83a8915100a310bbad5d9c1fc6397410259c94033b8318d548d9990"},
|
||||
{file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d716a7694ce1fa60b20bc10f35c4a22be446ef7f514c8dbc8f858b61976de2fb"},
|
||||
{file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:49685bf9a55d1ab34bd8423ea22db836ba43a181ac6b045ac4272093d5cb874e"},
|
||||
{file = "zstandard-0.18.0-cp38-cp38-win32.whl", hash = "sha256:1af1268a7dc870eb27515fb8db1f3e6c5a555d2b7bcc476fc3bab8886c7265ab"},
|
||||
{file = "zstandard-0.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:1dc2d3809e763055a1a6c1a73f2b677320cc9a5aa1a7c6cfb35aee59bddc42d9"},
|
||||
{file = "zstandard-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eea18c1e7442f2aa9aff1bb84550dbb6a1f711faf6e48e7319de8f2b2e923c2a"},
|
||||
{file = "zstandard-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8677ffc6a6096cccbd892e558471c901fd821aba12b7fbc63833c7346f549224"},
|
||||
{file = "zstandard-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083dc08abf03807af9beeb2b6a91c23ad78add2499f828176a3c7b742c44df02"},
|
||||
{file = "zstandard-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c990063664c08169c84474acecc9251ee035871589025cac47c060ff4ec4bc1a"},
|
||||
{file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:533db8a6fac6248b2cb2c935e7b92f994efbdeb72e1ffa0b354432e087bb5a3e"},
|
||||
{file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb3cb8a082d62b8a73af42291569d266b05605e017a3d8a06a0e5c30b5f10f0"},
|
||||
{file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d6c85ca5162049ede475b7ec98e87f9390501d44a3d6776ddd504e872464ec25"},
|
||||
{file = "zstandard-0.18.0-cp39-cp39-win32.whl", hash = "sha256:75479e7c2b3eebf402c59fbe57d21bc400cefa145ca356ee053b0a08908c5784"},
|
||||
{file = "zstandard-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:d85bfabad444812133a92fc6fbe463e1d07581dba72f041f07a360e63808b23c"},
|
||||
{file = "zstandard-0.18.0.tar.gz", hash = "sha256:0ac0357a0d985b4ff31a854744040d7b5754385d1f98f7145c30e02c6865cb6f"},
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
[tool.poetry]
|
||||
name = "worthless"
|
||||
version = "3.0.0"
|
||||
description = "A worthless CLI launcher written in Python"
|
||||
authors = ["tretrauit <tretrauit@gmail.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
aiohttp = "^3.8.1"
|
||||
appdirs = "^1.4.4"
|
||||
detools = "^0.51.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.1.3"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
worthless = 'worthless.cli:main'
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
|
@ -1,3 +0,0 @@
|
|||
aiohttp==3.8.3
|
||||
appdirs~=1.4.4
|
||||
aiopath~=0.6.10
|
28
setup.py
28
setup.py
|
@ -1,28 +0,0 @@
|
|||
import pathlib
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
# The directory containing this file
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
|
||||
# The text of the README file
|
||||
README = (HERE / "README.md").read_text()
|
||||
|
||||
setup(
|
||||
name='worthless',
|
||||
version='2.2.22',
|
||||
packages=['worthless', 'worthless.classes', 'worthless.classes.launcher', 'worthless.classes.installer'],
|
||||
url='https://git.tretrauit.me/tretrauit/worthless-launcher',
|
||||
license='MIT License',
|
||||
author='tretrauit',
|
||||
author_email='tretrauit@gmail.org',
|
||||
description='A worthless CLI launcher written in Python.',
|
||||
long_description=README,
|
||||
long_description_content_type="text/markdown",
|
||||
classifiers=[
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3"
|
||||
],
|
||||
include_package_data=True,
|
||||
install_requires=["aiohttp", "appdirs", "aiopath"]
|
||||
)
|
|
@ -1,7 +1,4 @@
|
|||
from worthless import launcher, installer
|
||||
from worthless import launcher, game
|
||||
|
||||
Launcher = launcher.Launcher
|
||||
Installer = installer.Installer
|
||||
|
||||
|
||||
__version__ = "2.2.22"
|
||||
Game = game.Game
|
||||
|
|
|
@ -3,4 +3,4 @@ import asyncio
|
|||
from worthless import cli
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(cli.main())
|
||||
cli.main()
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from worthless.classes.installer import resource, game, latest, diff, voicepack
|
||||
|
||||
Resource = resource.Resource
|
||||
Game = game.Game
|
||||
Latest = latest.Latest
|
||||
|
|
|
@ -2,7 +2,9 @@ from worthless.classes.installer.voicepack import Voicepack
|
|||
|
||||
|
||||
class Diff:
|
||||
def __init__(self, name, version, path, size, md5, is_recommended_update, voice_packs, raw):
|
||||
def __init__(
|
||||
self, name, version, path, size, md5, is_recommended_update, voice_packs, raw
|
||||
):
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.path = path
|
||||
|
@ -18,7 +20,15 @@ class Diff:
|
|||
@staticmethod
|
||||
def from_dict(data):
|
||||
voice_packs = []
|
||||
for v in data['voice_packs']:
|
||||
for v in data["voice_packs"]:
|
||||
voice_packs.append(Voicepack.from_dict(v))
|
||||
return Diff(data["name"], data["version"], data["path"], data["size"], data["md5"],
|
||||
data["is_recommended_update"], voice_packs, data)
|
||||
return Diff(
|
||||
data["name"],
|
||||
data["version"],
|
||||
data["path"],
|
||||
data["size"],
|
||||
data["md5"],
|
||||
data["is_recommended_update"],
|
||||
voice_packs,
|
||||
data,
|
||||
)
|
||||
|
|
|
@ -12,8 +12,8 @@ class Game:
|
|||
def from_dict(data):
|
||||
try:
|
||||
diffs = []
|
||||
for diff in data['diffs']:
|
||||
for diff in data["diffs"]:
|
||||
diffs.append(Diff.from_dict(diff))
|
||||
return Game(Latest.from_dict(data['latest']), diffs, data)
|
||||
return Game(Latest.from_dict(data["latest"]), diffs, data)
|
||||
except (KeyError, ValueError, TypeError):
|
||||
return data
|
||||
|
|
|
@ -2,7 +2,19 @@ from worthless.classes.installer.voicepack import Voicepack
|
|||
|
||||
|
||||
class Latest:
|
||||
def __init__(self, name, version, path, size, md5, entry, voice_packs, decompressed_path, segments, raw):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
version,
|
||||
path,
|
||||
size,
|
||||
md5,
|
||||
entry,
|
||||
voice_packs,
|
||||
decompressed_path,
|
||||
segments,
|
||||
raw,
|
||||
):
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.path = path
|
||||
|
@ -15,15 +27,22 @@ class Latest:
|
|||
self.raw = raw
|
||||
|
||||
def get_name(self):
|
||||
name = self.path.split("/")[-1]
|
||||
if name == "":
|
||||
name = self.segments[0]["path"].split("/")[-1][:-4]
|
||||
return name
|
||||
return self.path.split("/")[-1]
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data):
|
||||
voice_packs = []
|
||||
for v in data['voice_packs']:
|
||||
for v in data["voice_packs"]:
|
||||
voice_packs.append(Voicepack.from_dict(v))
|
||||
return Latest(data["name"], data["version"], data["path"], data["size"], data["md5"], data["entry"],
|
||||
voice_packs, data["decompressed_path"], data["segments"], data)
|
||||
return Latest(
|
||||
data["name"],
|
||||
data["version"],
|
||||
data["path"],
|
||||
data["size"],
|
||||
data["md5"],
|
||||
data["entry"],
|
||||
voice_packs,
|
||||
data["decompressed_path"],
|
||||
data["segments"],
|
||||
data,
|
||||
)
|
||||
|
|
|
@ -14,7 +14,18 @@ class Resource:
|
|||
- :class:`worthless.classes.launcher.qq.QQ` post: The launcher QQ posts information.
|
||||
- :class:`dict` raw: The launcher raw information.
|
||||
"""
|
||||
def __init__(self, game, plugin, web_url, force_update, pre_download_game, deprecated_packages, sdk, raw):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
game,
|
||||
plugin,
|
||||
web_url,
|
||||
force_update,
|
||||
pre_download_game,
|
||||
deprecated_packages,
|
||||
sdk,
|
||||
raw,
|
||||
):
|
||||
self.game = game
|
||||
self.plugin = plugin
|
||||
self.web_url = web_url
|
||||
|
@ -26,5 +37,13 @@ class Resource:
|
|||
|
||||
@staticmethod
|
||||
def from_dict(data):
|
||||
return Resource(Game.from_dict(data['game']), data['plugin'], data['web_url'], data['force_update'],
|
||||
Game.from_dict(data['pre_download_game']), data['deprecated_packages'], data['sdk'], data)
|
||||
return Resource(
|
||||
Game.from_dict(data["game"]),
|
||||
data["plugin"],
|
||||
data["web_url"],
|
||||
data["force_update"],
|
||||
Game.from_dict(data["pre_download_game"]),
|
||||
data["deprecated_packages"],
|
||||
data["sdk"],
|
||||
data,
|
||||
)
|
||||
|
|
|
@ -12,4 +12,11 @@ class Voicepack:
|
|||
|
||||
@staticmethod
|
||||
def from_dict(data):
|
||||
return Voicepack(data["language"], data["name"], data["path"], data["size"], data["md5"], data)
|
||||
return Voicepack(
|
||||
data["language"],
|
||||
data["name"],
|
||||
data["path"],
|
||||
data["size"],
|
||||
data["md5"],
|
||||
data,
|
||||
)
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
from worthless.classes.launcher import background, banner, iconbutton, iconotherlink, info, post, qq
|
||||
from worthless.classes.launcher import (
|
||||
background,
|
||||
banner,
|
||||
iconbutton,
|
||||
iconotherlink,
|
||||
info,
|
||||
post,
|
||||
qq,
|
||||
)
|
||||
|
||||
Background = background.Background
|
||||
Banner = banner.Banner
|
||||
IconButton = iconbutton.IconButton
|
||||
|
|
|
@ -29,7 +29,13 @@ class Background:
|
|||
self.raw = raw
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data) -> 'Background':
|
||||
def from_dict(data) -> "Background":
|
||||
"""Creates a launcher background from a dictionary."""
|
||||
return Background(data["background"], data["icon"], data["url"],
|
||||
data["version"], data["bg_checksum"], data)
|
||||
return Background(
|
||||
data["background"],
|
||||
data["icon"],
|
||||
data["url"],
|
||||
data["version"],
|
||||
data["bg_checksum"],
|
||||
data,
|
||||
)
|
||||
|
|
|
@ -15,6 +15,7 @@ class Banner:
|
|||
- :class:`str` name: The banner name.
|
||||
- :class:`dict` raw: The banner raw information.
|
||||
"""
|
||||
|
||||
def __init__(self, banner_id, name, img, url, order, raw):
|
||||
self.banner_id = banner_id
|
||||
self.name = name
|
||||
|
@ -24,6 +25,13 @@ class Banner:
|
|||
self.raw = raw
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data) -> 'Banner':
|
||||
def from_dict(data) -> "Banner":
|
||||
"""Creates a launcher banner from a dictionary."""
|
||||
return Banner(data["banner_id"], data["name"], data["img"], data["url"], data["order"], data)
|
||||
return Banner(
|
||||
data["banner_id"],
|
||||
data["name"],
|
||||
data["img"],
|
||||
data["url"],
|
||||
data["order"],
|
||||
data,
|
||||
)
|
||||
|
|
|
@ -20,7 +20,9 @@ class IconButton:
|
|||
- :class:`dict` raw: The launcher background raw information.
|
||||
"""
|
||||
|
||||
def __init__(self, icon_id, img, tittle, url, qr_img, qr_desc, img_hover, other_links, raw):
|
||||
def __init__(
|
||||
self, icon_id, img, tittle, url, qr_img, qr_desc, img_hover, other_links, raw
|
||||
):
|
||||
"""Inits the launcher icon class"""
|
||||
self.icon_id = icon_id
|
||||
self.img = img
|
||||
|
@ -33,10 +35,19 @@ class IconButton:
|
|||
self.raw = raw
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data) -> 'IconButton':
|
||||
def from_dict(data) -> "IconButton":
|
||||
"""Creates a launcher background from a dictionary."""
|
||||
other_links = []
|
||||
for link in data['other_links']:
|
||||
for link in data["other_links"]:
|
||||
other_links.append(IconOtherLink.from_dict(link))
|
||||
return IconButton(data["icon_id"], data["img"], data["tittle"], data["url"], data["qr_img"],
|
||||
data["qr_desc"], data["img_hover"], other_links, data)
|
||||
return IconButton(
|
||||
data["icon_id"],
|
||||
data["img"],
|
||||
data["tittle"],
|
||||
data["url"],
|
||||
data["qr_img"],
|
||||
data["qr_desc"],
|
||||
data["img_hover"],
|
||||
other_links,
|
||||
data,
|
||||
)
|
||||
|
|
|
@ -4,6 +4,6 @@ class IconOtherLink:
|
|||
self.url = url
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data) -> 'IconOtherLink':
|
||||
def from_dict(data) -> "IconOtherLink":
|
||||
"""Creates a launcher icon other link from a dictionary."""
|
||||
return IconOtherLink(data["title"], data["url"])
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from worthless.classes.launcher import background, banner, iconbutton, post, qq
|
||||
|
||||
Background = background.Background
|
||||
Banner = banner.Banner
|
||||
IconButton = iconbutton.IconButton
|
||||
|
@ -20,8 +21,16 @@ class Info:
|
|||
- :class:`worthless.classes.launcher.qq.QQ` post: The launcher QQ posts information.
|
||||
- :class:`dict` raw: The launcher raw information.
|
||||
"""
|
||||
def __init__(self, lc_background: Background, lc_banner: list[Banner], icon: list[IconButton], lc_post: list[Post],
|
||||
lc_qq: list[QQ], raw: dict):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
lc_background: Background,
|
||||
lc_banner: list[Banner],
|
||||
icon: list[IconButton],
|
||||
lc_post: list[Post],
|
||||
lc_qq: list[QQ],
|
||||
raw: dict,
|
||||
):
|
||||
self.background = lc_background
|
||||
self.banner = lc_banner
|
||||
self.icon = icon
|
||||
|
@ -45,4 +54,3 @@ class Info:
|
|||
for q in data["qq"]:
|
||||
lc_qq.append(QQ.from_dict(q))
|
||||
return Info(bg, lc_banner, lc_icon, lc_post, lc_qq, data)
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ class Post:
|
|||
- :class:`str` title: The post title.
|
||||
- :class:`dict` raw: The post raw information.
|
||||
"""
|
||||
|
||||
def __init__(self, post_id, post_type, tittle, url, show_time, order, title, raw):
|
||||
self.post_id = post_id
|
||||
self.type = post_type # Shadow built-in name `type`
|
||||
|
@ -32,7 +33,15 @@ class Post:
|
|||
self.raw = raw
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data) -> 'Post':
|
||||
def from_dict(data) -> "Post":
|
||||
"""Creates a launcher post from a dictionary."""
|
||||
return Post(data["post_id"], data["type"], data["tittle"], data["url"],
|
||||
data["show_time"], data["order"], data["title"], data)
|
||||
return Post(
|
||||
data["post_id"],
|
||||
data["type"],
|
||||
data["tittle"],
|
||||
data["url"],
|
||||
data["show_time"],
|
||||
data["order"],
|
||||
data["title"],
|
||||
data,
|
||||
)
|
||||
|
|
|
@ -11,6 +11,7 @@ class QQ:
|
|||
- :class:`str` code: The QQ post url.
|
||||
- :class:`dict` raw: The launcher raw information.
|
||||
"""
|
||||
|
||||
def __init__(self, qq_id, name, number, code, raw):
|
||||
self.qq_id = qq_id
|
||||
self.name = name
|
||||
|
@ -19,7 +20,7 @@ class QQ:
|
|||
self.raw = raw
|
||||
|
||||
@staticmethod
|
||||
def from_dict(raw: dict) -> 'QQ':
|
||||
def from_dict(raw: dict) -> "QQ":
|
||||
"""Creates a QQ object from a dictionary
|
||||
|
||||
Args:
|
||||
|
@ -28,8 +29,8 @@ class QQ:
|
|||
Returns:
|
||||
QQ: The QQ object
|
||||
"""
|
||||
qq_id = raw.get('qq_id')
|
||||
name = raw.get('name')
|
||||
number = int(raw.get('number'))
|
||||
code = raw.get('code')
|
||||
qq_id = raw.get("qq_id")
|
||||
name = raw.get("name")
|
||||
number = int(raw.get("number"))
|
||||
code = raw.get("code")
|
||||
return QQ(qq_id, name, number, code, raw)
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
class mhyResponse:
|
||||
"""Simple class for wrapping miHoYo web response
|
||||
Currently not used for anything.
|
||||
"""
|
||||
def __init__(self, retcode, message, data):
|
||||
self.retcode = retcode
|
||||
self.message = message
|
||||
self.data = data
|
409
worthless/cli.py
409
worthless/cli.py
|
@ -1,407 +1,2 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
|
||||
import appdirs
|
||||
from pathlib import Path
|
||||
from worthless.launcher import Launcher
|
||||
from worthless.installer import Installer
|
||||
from worthless.patcher import Patcher
|
||||
import worthless.constants as constants
|
||||
|
||||
|
||||
class UI:
|
||||
def __init__(self, gamedir: str, noconfirm: bool, tempdir: str | Path = None, pre_download: bool = False, download_chunk: int = 8192) -> None:
|
||||
self._vo_version = None
|
||||
self._noconfirm = noconfirm
|
||||
self._gamedir = gamedir
|
||||
self._launcher = Launcher(gamedir)
|
||||
self._installer = Installer(gamedir, data_dir=tempdir)
|
||||
self._patcher = Patcher(gamedir, data_dir=tempdir)
|
||||
self._pre_download = pre_download
|
||||
if self._pre_download:
|
||||
print("Pre-download is enabled, use at your own risk!")
|
||||
self._installer.set_download_chunk(download_chunk)
|
||||
|
||||
def _ask(self, question):
|
||||
if self._noconfirm:
|
||||
# Fake dialog
|
||||
print(question + " [Y/n]:")
|
||||
return True
|
||||
|
||||
# random text
|
||||
answer = "kurumi3"
|
||||
while answer.lower() not in ['y', 'n', '']:
|
||||
answer = input(question + " [Y/n]: ")
|
||||
return answer.lower() == 'y' or answer == ''
|
||||
|
||||
def override_game_version(self, version: str):
|
||||
self._installer._version = version
|
||||
|
||||
def override_voiceover_version(self, version: str):
|
||||
self._vo_version = version
|
||||
|
||||
async def get_game_version(self):
|
||||
print(await self._installer.get_game_version())
|
||||
|
||||
async def block_telemetry(self):
|
||||
print("Checking for available telemetry to block...")
|
||||
try:
|
||||
await self._patcher.block_telemetry()
|
||||
except ValueError:
|
||||
print("No telemetry to block.")
|
||||
else:
|
||||
print("Telemetry blocked.")
|
||||
|
||||
async def check_telemetry(self):
|
||||
block_status = await self._patcher.is_telemetry_blocked()
|
||||
if not block_status:
|
||||
print("Telemetry is blocked.")
|
||||
else:
|
||||
print("Telemetry is not blocked, you need to block these hosts below.")
|
||||
for hosts in block_status:
|
||||
print(hosts)
|
||||
|
||||
async def _update_from_archive(self, filepath):
|
||||
print("Reverting patches if patched...")
|
||||
await self._patcher.revert_patch(True)
|
||||
print("Updating game from archive (This may takes some time)...")
|
||||
await self._installer.update_game(filepath)
|
||||
self._installer.set_version_config()
|
||||
|
||||
async def _install_from_archive(self, filepath, force_reinstall):
|
||||
print("Installing game from archive (This may takes some time)...")
|
||||
print(filepath)
|
||||
await self._installer.install_game(filepath, force_reinstall)
|
||||
self._installer.set_version_config()
|
||||
|
||||
async def _apply_voiceover_from_archive(self, filepath):
|
||||
print("Applying voiceover from archive (This may takes some time)...")
|
||||
print("Voiceover archive:", filepath)
|
||||
await self._installer.apply_voiceover(filepath)
|
||||
|
||||
async def install_voiceover_from_file(self, filepath):
|
||||
print("Archive voiceover language: {} ({})".format(
|
||||
await self._installer.get_voiceover_archive_language(filepath),
|
||||
"Full archive" if await self._installer.get_voiceover_archive_type(filepath) else "Update archive"))
|
||||
if not self._ask("Do you want to apply this voiceover pack? ({})".format(filepath)):
|
||||
print("Aborting apply process.")
|
||||
return
|
||||
await self._apply_voiceover_from_archive(filepath)
|
||||
print("Voiceover applied successfully.")
|
||||
|
||||
async def revert_patch(self):
|
||||
print("Reverting patches...")
|
||||
await self._patcher.revert_patch(True)
|
||||
print("Patches reverted.")
|
||||
|
||||
async def patch_game(self, login_fix: bool = False):
|
||||
print("NOTE: Hereby you are violating the game's Terms of Service!")
|
||||
print("Do not patch the game if you don't know what you are doing!")
|
||||
if not self._ask("Do you want to patch the game? (This will overwrite your game files!)"):
|
||||
print("Aborting patch process.")
|
||||
return
|
||||
await self.block_telemetry()
|
||||
print("Updating patches...")
|
||||
await self._patcher.download_patch()
|
||||
print("Patching game...")
|
||||
await self._patcher.apply_patch(login_fix)
|
||||
print("Game patched.")
|
||||
print("Please refrain from sharing this project to public, thank you.")
|
||||
|
||||
async def install_from_file(self, filepath):
|
||||
gamever = await self._installer.get_game_version()
|
||||
print("Archive game version:", await self._installer.get_game_archive_version(filepath))
|
||||
if gamever:
|
||||
print("Current game installation detected. ({})".format(await self._installer.get_game_version()))
|
||||
if not self._ask("Do you want to update the game? ({})".format(filepath)):
|
||||
print("Aborting update process.")
|
||||
return
|
||||
await self._update_from_archive(filepath)
|
||||
print("Game updated successfully.")
|
||||
else:
|
||||
print("No game installation detected.")
|
||||
if not self._ask("Do you want to install the game? ({})".format(filepath)):
|
||||
print("Aborting installation process.")
|
||||
return
|
||||
await self._install_from_archive(filepath, False)
|
||||
print("Game installed successfully.")
|
||||
|
||||
async def download_patch(self):
|
||||
print("Downloading patches...")
|
||||
await self._patcher.download_patch()
|
||||
|
||||
async def download_game(self):
|
||||
print("Downloading full game (This will take a long time)...")
|
||||
await self._installer.download_full_game(self._pre_download)
|
||||
|
||||
async def download_game_update(self):
|
||||
print("Downloading game update (This will take a long time)...")
|
||||
await self._installer.download_game_update(pre_download=self._pre_download)
|
||||
|
||||
async def download_voiceover(self, languages: str):
|
||||
res_info = await self._launcher.get_resource_info()
|
||||
for lng in languages.split(" "):
|
||||
for vo in res_info.game.latest.voice_packs:
|
||||
if not self._installer.voiceover_lang_translate(lng) == vo.language:
|
||||
continue
|
||||
print("Downloading voiceover pack for {} (This will take a long time)...".format(lng))
|
||||
await self._installer.download_full_voiceover(lng, pre_download=self._pre_download)
|
||||
|
||||
async def download_voiceover_update(self, languages: str):
|
||||
res_info = await self._launcher.get_resource_info()
|
||||
for lng in languages.split(" "):
|
||||
for vo in res_info.game.latest.voice_packs:
|
||||
if not self._installer.voiceover_lang_translate(lng) == vo.language:
|
||||
continue
|
||||
print("Downloading voiceover update pack for {} (This will take a long time)...".format(lng))
|
||||
await self._installer.download_voiceover_update(lng, from_version=self._vo_version, pre_download=self._pre_download)
|
||||
|
||||
async def install_game(self, forced: bool = False):
|
||||
res_info = await self._launcher.get_resource_info()
|
||||
game = res_info.game
|
||||
if self._pre_download:
|
||||
game = res_info.pre_download_game
|
||||
print("Latest game version: {}".format(game.latest.version))
|
||||
if not self._ask("Do you want to install the game?"):
|
||||
print("Aborting game installation process.")
|
||||
return
|
||||
await self.download_game()
|
||||
print("Game archive:", game.latest.get_name())
|
||||
await self._install_from_archive(self._installer.temp_path.joinpath(game.latest.get_name()), forced)
|
||||
|
||||
async def install_voiceover(self, languages: str):
|
||||
res_info = await self._launcher.get_resource_info()
|
||||
for lng in languages.split(" "):
|
||||
for vo in res_info.game.latest.voice_packs:
|
||||
if not self._installer.voiceover_lang_translate(lng) == vo.language:
|
||||
continue
|
||||
if not self._ask("Do you want to install this voiceover pack? ({})".format(lng)):
|
||||
print("Aborting voiceover installation process.")
|
||||
break
|
||||
print("Downloading voiceover pack (This will take a long time)...")
|
||||
await self._installer.download_full_voiceover(lng, pre_download=self._pre_download)
|
||||
await self._apply_voiceover_from_archive(
|
||||
self._installer.temp_path.joinpath(vo.get_name())
|
||||
)
|
||||
|
||||
async def update_game(self):
|
||||
game_ver = await self._installer.get_game_version()
|
||||
if not game_ver:
|
||||
await self.install_game()
|
||||
return
|
||||
print("Current game installation detected: {}".format(game_ver))
|
||||
diff_archive = await self._installer.get_game_diff_archive()
|
||||
res_info = await self._launcher.get_resource_info()
|
||||
game = res_info.game
|
||||
if self._pre_download:
|
||||
game = res_info.pre_download_game
|
||||
if not diff_archive:
|
||||
print("No game updates available.")
|
||||
return
|
||||
print("Latest game version: {}".format(game.latest.version))
|
||||
if not self._ask("Do you want to update the game?"):
|
||||
print("Aborting game update process.")
|
||||
return
|
||||
print("Downloading game update (This will take a long time)...")
|
||||
await self._installer.download_game_update(pre_download=self._pre_download)
|
||||
print("Installing game update...")
|
||||
await self.install_from_file(self._installer.temp_path.joinpath(diff_archive.get_name()))
|
||||
|
||||
async def update_voiceover(self, languages: str | list):
|
||||
if isinstance(languages, str):
|
||||
languages = languages.split(" ")
|
||||
game_ver = self._vo_version or await self._installer.get_game_version()
|
||||
if not game_ver:
|
||||
print("Couldn't detect current game installation, is game installed?")
|
||||
return
|
||||
installed_voiceovers = await self._installer.get_installed_voiceovers()
|
||||
print(f"Installed voiceovers: {None if installed_voiceovers == [] else ', '.join(installed_voiceovers)}")
|
||||
for lng in languages:
|
||||
if self._installer.voiceover_lang_translate(lng, "locale") not in installed_voiceovers:
|
||||
await self.install_voiceover(lng)
|
||||
continue
|
||||
diff_archive = await self._installer.get_voiceover_diff_archive(lng, pre_download=self._pre_download)
|
||||
if not diff_archive:
|
||||
print("No voiceover updates available for {}.".format(lng))
|
||||
continue
|
||||
if not self._ask("Do you want to update this voiceover? ({})".format(lng)):
|
||||
print("Aborting this voiceover language update process.")
|
||||
continue
|
||||
print("Downloading voiceover update (This may takes some time)...")
|
||||
await self._installer.download_voiceover_update(lng, pre_download=self._pre_download)
|
||||
print("Installing voiceover update for {}...".format(lng))
|
||||
await self._apply_voiceover_from_archive(self._installer.temp_path.joinpath(diff_archive.get_name()))
|
||||
|
||||
async def update_game_voiceover(self, languages: str):
|
||||
await self.update_game()
|
||||
await self.update_voiceover(languages)
|
||||
|
||||
async def update_voiceover_all(self):
|
||||
await self.update_voiceover(await self._installer.get_installed_voiceovers())
|
||||
|
||||
async def update_all(self):
|
||||
await self.update_game()
|
||||
await self.update_voiceover(await self._installer.get_installed_voiceovers())
|
||||
|
||||
async def verify_game(self):
|
||||
game_ver = await self._installer.get_game_version()
|
||||
if not game_ver:
|
||||
print("Couldn't detect current game installation, is game installed?")
|
||||
return
|
||||
print("Verifying game contents... (This may takes a long time)")
|
||||
failed_files = await self._installer.verify_game(ignore_mismatch=True)
|
||||
if not failed_files:
|
||||
print("All good.")
|
||||
return
|
||||
print("Some game files got corrupted (mismatch md5), uh oh.")
|
||||
for file in failed_files:
|
||||
print("{}: expected {}, actual {}".format(file[0], file[1], file[2]))
|
||||
|
||||
async def clear_cache(self):
|
||||
if self._ask("Do you want to clear Installer cache (contains downloaded game files, etc)?"):
|
||||
await self._installer.clear_cache()
|
||||
if self._ask("Do you want to clear Patcher cache (contains files used to patch)?"):
|
||||
await self._patcher.clear_cache()
|
||||
|
||||
|
||||
async def main():
|
||||
default_dirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR)
|
||||
parser = argparse.ArgumentParser(prog="worthless", description="A worthless launcher written in Python.")
|
||||
parser.add_argument("-D", "--dir", action="store", type=Path, default=Path.cwd(),
|
||||
help="Specify the game directory (default current working directory)")
|
||||
parser.add_argument("-W", "--temporary-dir", action="store", type=Path, default=None,
|
||||
help="Specify the temporary directory (default {} and {})".format(default_dirs.user_data_dir,
|
||||
default_dirs.user_cache_dir))
|
||||
parser.add_argument("-S", "--install", action="store_true",
|
||||
help="Install/update the game (if not already installed, else do nothing)")
|
||||
parser.add_argument("-U", "--install-from-file", action="store", type=Path, default=None,
|
||||
help="Install the game from an archive (if not already installed, \
|
||||
else update from archive)")
|
||||
parser.add_argument("-Uv", "--install-voiceover-from-file", action="store", type=Path, default=None,
|
||||
help="Install the voiceover from an archive (if not already installed, \
|
||||
else update from archive)")
|
||||
parser.add_argument("-Sp", "--patch", action="store_true",
|
||||
help="Patch the game (if not already patched, else do nothing)")
|
||||
parser.add_argument("--login-fix", action="store_true",
|
||||
help="Patch the game to fix login issues (if not already patched, else do nothing)")
|
||||
parser.add_argument("-Sy", "--update", action="store", type=str, default="",
|
||||
help="Update the game and specified voiceover pack only (or install if not found)")
|
||||
parser.add_argument("-Sw", "--download-game", action="store_true",
|
||||
help="Download the full game to the temporary directory")
|
||||
parser.add_argument("-Swp", "--download-patch", action="store_true",
|
||||
help="Download/Update the game patch to the temporary directory")
|
||||
parser.add_argument("-Swv", "--download-voiceover", action="store", type=str,
|
||||
help="Download the full voiceover to the temporary directory")
|
||||
parser.add_argument("-Syw", "--download-game-update", action="store", type=str, default="",
|
||||
help="Download the game and the voiceover update to the temporary directory")
|
||||
parser.add_argument("-Sywv", "--download-voiceover-update", action="store", type=str,
|
||||
help="Download the voiceover update to the temporary directory")
|
||||
parser.add_argument("-Sv", "--update-voiceover", action="store", type=str,
|
||||
help="Update the voiceover pack only (or install if not found)")
|
||||
parser.add_argument("-Syu", "--update-all", action="store_true",
|
||||
help="Update the game and all installed voiceover packs (or install if not found)")
|
||||
parser.add_argument("-Scc", "--clear-cache", action="store_true", help="Clear cache used by worthless")
|
||||
parser.add_argument("-Rs", "--remove", action="store_true", help="Remove the game (if installed)")
|
||||
parser.add_argument("-Rp", "--remove-patch", action="store_true", help="Revert the game patch (if patched)")
|
||||
parser.add_argument("-Rv", "--remove-voiceover", action="store_true", help="Remove a Voiceover pack (if installed)")
|
||||
parser.add_argument("-V", "--verify", action="store_true", help="Verify the game installation")
|
||||
parser.add_argument("--predownload", action="store_true", help="Download the game for the next update", default=False)
|
||||
parser.add_argument("--get-game-version", action="store_true", help="Get the current game version")
|
||||
parser.add_argument("--no-overseas", action="store_true", help="Don't use overseas server")
|
||||
parser.add_argument("--check-telemetry", action="store_true", help="Check for the telemetry information")
|
||||
parser.add_argument("--from-ver", action="store", help="Override the detected game version", type=str, default=None)
|
||||
parser.add_argument("--from-vo-ver", action="store", help="Override the detected game version for voiceover "
|
||||
"detection", type=str, default=None)
|
||||
parser.add_argument("--noconfirm", action="store_true",
|
||||
help="Do not ask any for confirmation. (Ignored in interactive mode)")
|
||||
parser.add_argument("--download-chunk", action="store", help="Chunks to download (default 8192 bytes)", type=int, default=8192)
|
||||
args = parser.parse_args()
|
||||
if args.temporary_dir:
|
||||
args.temporary_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ui = UI(args.dir, args.noconfirm, args.temporary_dir, args.predownload, args.download_chunk)
|
||||
|
||||
if args.install and args.update:
|
||||
raise ValueError("Cannot specify both --install and --update arguments.")
|
||||
|
||||
if args.install_from_file and args.update:
|
||||
raise ValueError("Cannot specify both --install-from-file and --update arguments.")
|
||||
|
||||
if args.install_voiceover_from_file and args.update:
|
||||
raise ValueError("Cannot specify both --install-voiceover-from-file and --update arguments.")
|
||||
|
||||
if args.install_from_file and args.install:
|
||||
raise ValueError("Cannot specify both --install-from-file and --install arguments.")
|
||||
|
||||
if args.from_ver:
|
||||
ui.override_game_version(args.from_ver)
|
||||
|
||||
if args.from_vo_ver:
|
||||
ui.override_voiceover_version(args.from_vo_ver)
|
||||
|
||||
if args.get_game_version:
|
||||
await ui.get_game_version()
|
||||
|
||||
if args.check_telemetry:
|
||||
await ui.check_telemetry()
|
||||
|
||||
if args.clear_cache:
|
||||
await ui.clear_cache()
|
||||
|
||||
# Download
|
||||
|
||||
if args.download_game:
|
||||
await ui.download_game()
|
||||
|
||||
if args.download_patch:
|
||||
await ui.download_patch()
|
||||
|
||||
if args.download_voiceover:
|
||||
await ui.download_voiceover(args.download_voiceover)
|
||||
|
||||
if args.download_game_update:
|
||||
await ui.download_game_update()
|
||||
|
||||
if args.download_voiceover_update:
|
||||
await ui.download_voiceover_update(args.download_voiceover_update)
|
||||
|
||||
# Install
|
||||
|
||||
if args.install:
|
||||
await ui.install_game()
|
||||
|
||||
if args.install_from_file:
|
||||
await ui.install_from_file(args.install_from_file)
|
||||
|
||||
if args.install_voiceover_from_file:
|
||||
await ui.install_voiceover_from_file(args.install_voiceover_from_file)
|
||||
|
||||
# Update
|
||||
|
||||
if args.update_all:
|
||||
await ui.update_all()
|
||||
|
||||
if args.update:
|
||||
await ui.update_game_voiceover(args.update)
|
||||
|
||||
if args.update_voiceover:
|
||||
await ui.update_voiceover(args.update_voiceover)
|
||||
|
||||
# Patch
|
||||
|
||||
if args.patch:
|
||||
await ui.patch_game(args.login_fix)
|
||||
|
||||
if args.remove_patch:
|
||||
await ui.revert_patch()
|
||||
|
||||
# Verify
|
||||
|
||||
if args.verify:
|
||||
await ui.verify_game()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
def main():
|
||||
pass
|
||||
|
|
|
@ -1,30 +1,27 @@
|
|||
from appdirs import AppDirs
|
||||
|
||||
|
||||
APP_NAME="worthless"
|
||||
APP_AUTHOR="tretrauit"
|
||||
APPDIRS=AppDirs(APP_NAME, APP_AUTHOR)
|
||||
APP_NAME = "worthless"
|
||||
APP_AUTHOR = "tretrauit"
|
||||
APPDIRS = AppDirs(APP_NAME, APP_AUTHOR)
|
||||
LAUNCHER_API_URL_OS = "https://sdk-os-static.hoyoverse.com/hk4e_global/mdk/launcher/api"
|
||||
LAUNCHER_API_URL_CN = "https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api"
|
||||
HDIFFPATCH_GIT_URL="https://github.com/sisong/HDiffPatch"
|
||||
HDIFFPATCH_GIT_URL = "https://github.com/sisong/HDiffPatch"
|
||||
PATCH_LIST = {
|
||||
"Krock": "https://notabug.org/Krock/dawn",
|
||||
"y0soro": "https://notabug.org/y0soro/dawn"
|
||||
"y0soro": "https://notabug.org/y0soro/dawn",
|
||||
}
|
||||
TELEMETRY_URL_LIST = [
|
||||
"log-upload-os.mihoyo.com",
|
||||
"log-upload-eur.mihoyo.com",
|
||||
"log-upload-os.hoyoverse.com",
|
||||
"overseauspider.yuanshen.com"
|
||||
]
|
||||
TELEMETRY_URL_CN_LIST = [
|
||||
"log-upload.mihoyo.com",
|
||||
"uspider.yuanshen.com"
|
||||
"overseauspider.yuanshen.com",
|
||||
]
|
||||
TELEMETRY_URL_CN_LIST = ["log-upload.mihoyo.com", "uspider.yuanshen.com"]
|
||||
TELEMETRY_OPTIONAL_URL_LIST = [
|
||||
"prd-lender.cdp.internal.unity3d.com",
|
||||
"thind-prd-knob.data.ie.unity3d.com",
|
||||
"thind-gke-usc.prd.data.corp.unity3d.com",
|
||||
"cdp.cloud.unity3d.com",
|
||||
"remote-config-proxy-prd.uca.cloud.unity3d.com"
|
||||
"remote-config-proxy-prd.uca.cloud.unity3d.com",
|
||||
]
|
|
@ -0,0 +1,3 @@
|
|||
from . import gameenums as enums
|
||||
from ._game import *
|
||||
from .gamehelper import Helper
|
|
@ -0,0 +1,694 @@
|
|||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
import zipfile
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
|
||||
from worthless import constants, launcher
|
||||
from worthless.classes import installer
|
||||
from worthless.game.gameenums import (
|
||||
Variant,
|
||||
VoicepackArchiveType,
|
||||
UpdateStatus,
|
||||
InstallStatus,
|
||||
VoicepackArchiveLanguage,
|
||||
)
|
||||
from worthless.game.hdiffpatch import HDiffPatch
|
||||
from worthless.game.launcherconfig import LauncherConfig
|
||||
|
||||
_logger = logging.getLogger("worthless.Game")
|
||||
_game_version_regex = re.compile(r"([1-9]+\.[0-9]+\.[0-9]+)_\d+_\d+")
|
||||
|
||||
|
||||
def _calculate_md5(file: str | bytes | PathLike):
|
||||
file = Path(file)
|
||||
if not file.exists():
|
||||
return ""
|
||||
with file.open("rb") as f:
|
||||
file_hash = hashlib.md5()
|
||||
while chunk := f.read(8196):
|
||||
file_hash.update(chunk)
|
||||
return file_hash.hexdigest()
|
||||
|
||||
|
||||
def _read_version_from_game_file(globalgamemanagers: Path | bytes) -> str | None:
|
||||
"""
|
||||
Reads the version from the globalgamemanagers file. (Data/globalgamemanagers)
|
||||
|
||||
This function uses `An Anime Game Launcher` method to read the version:
|
||||
https://github.com/an-anime-team/an-anime-game-launcher/blob/main/src/ts/Game.ts#L26
|
||||
|
||||
Returns:
|
||||
Version as string "x.x.x" or None if not found.
|
||||
"""
|
||||
if isinstance(globalgamemanagers, Path):
|
||||
data = globalgamemanagers.read_text(encoding="ascii", errors="ignore")
|
||||
else:
|
||||
data = globalgamemanagers.decode(encoding="ascii", errors="ignore")
|
||||
result = _game_version_regex.search(data)
|
||||
if not result:
|
||||
return
|
||||
return result.group(1)
|
||||
|
||||
|
||||
def parse_pkg_version(pkg_version: str | bytes | PathLike) -> dict:
|
||||
"""
|
||||
Parse a pkg_version file
|
||||
|
||||
Args:
|
||||
pkg_version: The pkg_version file to be parsed
|
||||
Returns:
|
||||
A parsed pkg_version as a dict
|
||||
{"<file 1>": "<md5">, "<file 2>": "<md5>", ...}
|
||||
"""
|
||||
contents = Path(pkg_version).read_text()
|
||||
pkg_version = {}
|
||||
for content in contents.split("\r\n"):
|
||||
if not content.strip():
|
||||
continue
|
||||
info = json.loads(content)
|
||||
pkg_version.update({info["remoteName"]: info["md5"]})
|
||||
return pkg_version
|
||||
|
||||
|
||||
def voicepack_lang_translate(lang: str, base_language="game") -> str | None:
|
||||
"""
|
||||
Translates the voicepack language to the language code used by the game (and reverse)
|
||||
|
||||
Args:
|
||||
lang: Language to translate
|
||||
base_language: Base language type (game/locale/both)
|
||||
Returns:
|
||||
Translated language code such as "en-us" or "English(US)"
|
||||
"""
|
||||
if base_language == "game" or base_language == "both":
|
||||
match lang.lower():
|
||||
case "english(us)":
|
||||
return "en-us"
|
||||
case "japanese":
|
||||
return "ja-jp"
|
||||
case "chinese":
|
||||
return "zh-cn"
|
||||
case "korean":
|
||||
return "ko-kr"
|
||||
if base_language == "locale" or base_language == "both":
|
||||
match lang.lower().replace("_", "-"):
|
||||
case "en-us":
|
||||
return "English(US)"
|
||||
case "ja-jp":
|
||||
return "Japanese"
|
||||
case "zh-cn":
|
||||
return "Chinese"
|
||||
case "ko-kr":
|
||||
return "Korean"
|
||||
|
||||
|
||||
def get_voicepack_archive_language(
|
||||
archive: str | bytes | PathLike,
|
||||
) -> VoicepackArchiveLanguage:
|
||||
"""
|
||||
Gets voicepack archive language.
|
||||
|
||||
Args:
|
||||
archive: The voicepack archive to get type
|
||||
Returns:
|
||||
VoicepackArchiveLanguage
|
||||
"""
|
||||
if not isinstance(archive, Path):
|
||||
archive = Path(archive).resolve()
|
||||
if not archive.exists():
|
||||
raise FileNotFoundError(f"Voiceover archive {archive} not found")
|
||||
with zipfile.ZipFile(archive, "r") as f:
|
||||
for file in zipfile.Path(f).iterdir():
|
||||
if file.name.startswith("Audio_") and file.name.endswith("_pkg_version"):
|
||||
# Audio_<language>_pkg_version
|
||||
return VoicepackArchiveLanguage(file.name.split("_")[1])
|
||||
|
||||
|
||||
def get_voicepack_archive_type(archive: str | bytes | PathLike) -> VoicepackArchiveType:
|
||||
"""
|
||||
Gets voicepack archive type.
|
||||
|
||||
Args:
|
||||
archive: The voicepack archive to get type
|
||||
Returns:
|
||||
VoicepackArchiveType.UPDATE for update archive, and VoicepackArchiveType.FULL for a full voicepack archive.
|
||||
"""
|
||||
vo_lang = get_voicepack_archive_language(archive)
|
||||
with zipfile.ZipFile(archive, "r") as f:
|
||||
archive_path = zipfile.Path(f)
|
||||
files = f.read("Audio_{}_pkg_version".format(vo_lang)).decode().split("\n")
|
||||
for file in files:
|
||||
if (
|
||||
file.strip()
|
||||
and not archive_path.joinpath(json.loads(file)["remoteName"]).exists()
|
||||
):
|
||||
return VoicepackArchiveType.UPDATE
|
||||
return VoicepackArchiveType.FULL
|
||||
|
||||
|
||||
def verify_game_file(
|
||||
file: str | bytes | PathLike,
|
||||
game_dir: str | bytes | PathLike,
|
||||
pkg_version: dict[str, str] | str | bytes | PathLike,
|
||||
ignore_mismatch=True,
|
||||
) -> (str, str, str) | None:
|
||||
"""
|
||||
Verifies a game file
|
||||
|
||||
It is strongly recommended to parse pkg_version with parse_pkg_version and use
|
||||
that as pkg_version argument if you're planning to verify multiple game files
|
||||
to save resource and verify faster.
|
||||
|
||||
Args:
|
||||
file: File to verify (e.g. GenshinImpact_Data/sharedassets0.assets.resS)
|
||||
game_dir: Game directory
|
||||
pkg_version: pkg_version file to read MD5 from, if not specified it'll use
|
||||
the file from the current game installation
|
||||
ignore_mismatch: Do not raise exception if a file has mismatch MD5, instead
|
||||
return as a (file, expected md5, actual md5) tuple
|
||||
Returns:
|
||||
None if the file is good, (file, expected md5, actual md5) if the file has mismatch md5
|
||||
Raises:
|
||||
FileNotFoundError if the file to verify doesn't exist
|
||||
"""
|
||||
game_dir = Path(game_dir)
|
||||
file_full = game_dir.joinpath(file)
|
||||
if not isinstance(pkg_version, dict):
|
||||
pkg_version = parse_pkg_version(pkg_version=pkg_version)
|
||||
if not pkg_version.get(file):
|
||||
raise FileNotFoundError(file)
|
||||
|
||||
file_md5 = _calculate_md5(file_full)
|
||||
if file_md5 == pkg_version[file]:
|
||||
return None
|
||||
if ignore_mismatch:
|
||||
return file, pkg_version[file], file_md5
|
||||
raise ValueError(
|
||||
f"MD5 does not match for {file}, expected md5: {pkg_version[file]}, actual md5: {file_md5}"
|
||||
)
|
||||
|
||||
|
||||
async def verify_game_files(
|
||||
game_dir: str | bytes | PathLike,
|
||||
pkg_version: dict[str, str] | str | bytes | PathLike,
|
||||
ignore_mismatch=True,
|
||||
):
|
||||
if not isinstance(game_dir, Path):
|
||||
game_dir = Path(game_dir)
|
||||
if not isinstance(pkg_version, dict):
|
||||
pkg_version = parse_pkg_version(pkg_version=pkg_version)
|
||||
|
||||
# Wrapper for catching FileNotFoundError
|
||||
def verify(f):
|
||||
try:
|
||||
result = verify_game_file(
|
||||
file=f,
|
||||
game_dir=game_dir,
|
||||
pkg_version=pkg_version,
|
||||
ignore_mismatch=ignore_mismatch,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
return result
|
||||
|
||||
verify_jobs = []
|
||||
for file in pkg_version:
|
||||
verify_jobs.append(asyncio.to_thread(verify, file))
|
||||
|
||||
verify_result = await asyncio.gather(*verify_jobs)
|
||||
failed_files = []
|
||||
for file in verify_result:
|
||||
if file is not None:
|
||||
failed_files.append(file)
|
||||
|
||||
return failed_files
|
||||
|
||||
|
||||
class Game:
|
||||
"""
|
||||
Manages game & voicepacks installation.
|
||||
|
||||
This class handles installing and updating game & voicepacks installation.
|
||||
|
||||
Args:
|
||||
game_dir: Game directory
|
||||
variant: Game variant
|
||||
cache_dir: Cache directory, if not specified it'll automatically detect your
|
||||
system cache directory and use that.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
game_dir: str | bytes | PathLike = None,
|
||||
variant: Variant = None,
|
||||
cache_dir: str | bytes | PathLike = None,
|
||||
):
|
||||
if not game_dir:
|
||||
game_dir = Path.cwd()
|
||||
if not isinstance(game_dir, Path):
|
||||
game_dir = Path(game_dir)
|
||||
self._game_dir = game_dir
|
||||
if cache_dir:
|
||||
if not isinstance(cache_dir, Path):
|
||||
cache_dir = Path(cache_dir)
|
||||
self._cache_path = cache_dir.joinpath("Installer/")
|
||||
else:
|
||||
self._cache_path = Path(constants.APPDIRS.user_cache_dir).joinpath(
|
||||
"Installer"
|
||||
)
|
||||
Path(self._cache_path).mkdir(parents=True, exist_ok=True)
|
||||
self._variant = variant
|
||||
self._version = None
|
||||
self._launcher = launcher.Launcher(variant=variant)
|
||||
self._hdiffpatch = HDiffPatch(data_dir=cache_dir)
|
||||
self._game_config_file = self._game_dir.joinpath("config.ini")
|
||||
self._game_config = LauncherConfig(
|
||||
self._game_config_file, game_version=self.version
|
||||
)
|
||||
|
||||
def _read_version_from_config(self) -> str | None:
|
||||
"""
|
||||
Reads the version from config.ini
|
||||
|
||||
Returns:
|
||||
Version as string "x.x.x" or None if not found.
|
||||
"""
|
||||
if not self._game_config_file.exists():
|
||||
raise FileNotFoundError(f"Config file {self._game_config_file} not found")
|
||||
return self._game_config.get("General", "game_version", fallback=None)
|
||||
|
||||
async def get_resource_info(self) -> installer.Resource:
|
||||
"""
|
||||
Alias for `launcher.get_resource_info()`
|
||||
"""
|
||||
return await self._launcher.get_resource_info()
|
||||
|
||||
async def get_server_game_info(self, pre_download: bool = False) -> installer.Game:
|
||||
game_resource = await self.get_resource_info()
|
||||
game = game_resource.game
|
||||
if not pre_download:
|
||||
return game
|
||||
game = game_resource.pre_download_game
|
||||
if game is None:
|
||||
raise RuntimeError("Pre-download version is not available.")
|
||||
|
||||
@property
|
||||
def version(self) -> str | None:
|
||||
"""
|
||||
Gets the current game version
|
||||
|
||||
Returns:
|
||||
Version as string "x.x.x" or None if not found.
|
||||
"""
|
||||
if not self._version:
|
||||
self._version = self.get_current_game_version()
|
||||
return self._version
|
||||
|
||||
@property
|
||||
def variant(self) -> Variant:
|
||||
"""
|
||||
Gets the current game variant
|
||||
|
||||
This property is a shorthand for get_current_game_variant()
|
||||
|
||||
Returns:
|
||||
GameVariant
|
||||
"""
|
||||
if not self._variant:
|
||||
self._version = self.get_current_game_variant()
|
||||
return self._variant
|
||||
|
||||
@property
|
||||
def cache_path(self) -> Path:
|
||||
return self._cache_path
|
||||
|
||||
@staticmethod
|
||||
def voicepack_lang_translate(lang: str, base_language="game") -> str | None:
|
||||
"""
|
||||
This function is an alias to worthless.game.voicepack_lang_translate
|
||||
"""
|
||||
return voicepack_lang_translate(lang=lang, base_language=base_language)
|
||||
|
||||
@staticmethod
|
||||
def get_voicepack_archive_language(
|
||||
archive: str | bytes | PathLike,
|
||||
) -> VoicepackArchiveLanguage:
|
||||
"""
|
||||
This function is an alias to worthless.game.get_voicepack_archive_language
|
||||
"""
|
||||
return get_voicepack_archive_language(archive=archive)
|
||||
|
||||
@staticmethod
|
||||
def get_voicepack_archive_type(
|
||||
archive: str | bytes | PathLike,
|
||||
) -> VoicepackArchiveType:
|
||||
"""
|
||||
This function is an alias to worthless.game.get_voicepack_archive_type
|
||||
"""
|
||||
return get_voicepack_archive_type(archive=archive)
|
||||
|
||||
def get_game_data_name(self) -> str:
|
||||
"""
|
||||
Gets the game data path name
|
||||
|
||||
Returns:
|
||||
A string containing game data path (e.g. GenshinImpact_Data)
|
||||
"""
|
||||
match self.variant:
|
||||
case Variant.INTERNATIONAL:
|
||||
return "GenshinImpact_Data"
|
||||
case Variant.CHINESE:
|
||||
return "YuanShen_Data"
|
||||
case Variant.BILIBILI:
|
||||
return "YuanShen_Data"
|
||||
|
||||
def get_game_data_path(self) -> Path:
|
||||
"""
|
||||
Gets the game data path
|
||||
|
||||
Returns:
|
||||
A Path object with the game data path.
|
||||
"""
|
||||
return self._game_dir.joinpath(self.get_game_data_name())
|
||||
|
||||
def get_archive_game_version(
|
||||
self, game_archive: str | bytes | PathLike
|
||||
) -> str | None:
|
||||
"""
|
||||
Gets the game version in the archive
|
||||
|
||||
Returns:
|
||||
Version as string "x.x.x" or None if not found.
|
||||
"""
|
||||
game_archive = Path(game_archive)
|
||||
if not game_archive.is_file():
|
||||
raise FileNotFoundError(f"Game archive {game_archive} not found")
|
||||
with zipfile.ZipFile(game_archive, "r") as f:
|
||||
return _read_version_from_game_file(
|
||||
f.read(self.get_game_data_name() + "globalgamemanagers")
|
||||
)
|
||||
|
||||
def get_current_game_variant(self) -> Variant:
|
||||
"""
|
||||
Gets the current game variant
|
||||
|
||||
Returns:
|
||||
GameVariant
|
||||
"""
|
||||
if self._game_dir.joinpath("GenshinImpact.exe").is_file():
|
||||
return Variant.INTERNATIONAL
|
||||
if not self._game_dir.joinpath("YuanShen.exe").is_file():
|
||||
raise FileNotFoundError("Game installation not found.")
|
||||
# We can't depend on get_game_data_name() because it depends on self._variant
|
||||
# which depends on this function.
|
||||
if self._game_dir.joinpath("YuanShen_Data/Plugins/PCGameSDK.dll").is_file():
|
||||
return Variant.BILIBILI
|
||||
return Variant.CHINESE
|
||||
|
||||
def get_current_game_version(self) -> str | None:
|
||||
"""
|
||||
Gets the current game version
|
||||
|
||||
Returns:
|
||||
Version as string "x.x.x" or None if not found.
|
||||
"""
|
||||
globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers")
|
||||
if not globalgamemanagers.exists():
|
||||
try:
|
||||
return self._read_version_from_config()
|
||||
except FileNotFoundError:
|
||||
return
|
||||
return _read_version_from_game_file(globalgamemanagers)
|
||||
|
||||
def get_installed_voicepacks(self) -> list[str]:
|
||||
"""
|
||||
Returns a list of installed voicepacks.
|
||||
|
||||
:return: List of installed voicepacks
|
||||
"""
|
||||
voicepacks = []
|
||||
for file in (
|
||||
self.get_game_data_path()
|
||||
.joinpath("StreamingAssets/Audio/GeneratedSoundBanks/Windows")
|
||||
.iterdir()
|
||||
):
|
||||
if file.is_dir():
|
||||
voicepacks.append(file.name)
|
||||
return voicepacks
|
||||
|
||||
async def update_game(self, game_archive: str | bytes | PathLike, callback=None):
|
||||
if not self.get_game_data_path().exists():
|
||||
raise FileNotFoundError(f"Game not found in {self._game_dir}")
|
||||
if not isinstance(game_archive, Path):
|
||||
game_archive = Path(game_archive).resolve()
|
||||
if not game_archive.exists():
|
||||
raise FileNotFoundError(f"Update archive {game_archive} not found")
|
||||
|
||||
def call(status, f=None):
|
||||
if not callback:
|
||||
return
|
||||
callback(status=status, file=f)
|
||||
|
||||
if not self._hdiffpatch.get_hpatchz_executable():
|
||||
call(UpdateStatus.DOWNLOADING_PATCHER)
|
||||
await self._hdiffpatch.download_latest_release()
|
||||
|
||||
archive = zipfile.ZipFile(game_archive, "r")
|
||||
files = archive.namelist()
|
||||
# Don't extract these files (they're useless and if the game isn't patched then it'll
|
||||
# raise 31-4xxx error in-game)
|
||||
call(UpdateStatus.PREPARING_PATCH)
|
||||
for file in ["deletefiles.txt", "hdifffiles.txt"]:
|
||||
try:
|
||||
files.remove(file)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# hdiffpatch implementation
|
||||
hdifffiles = []
|
||||
for x in (
|
||||
(await asyncio.to_thread(archive.read, "hdifffiles.txt"))
|
||||
.decode()
|
||||
.split("\n")
|
||||
):
|
||||
if x:
|
||||
hdifffiles.append(json.loads(x)["remoteName"])
|
||||
patch_jobs = []
|
||||
for file in hdifffiles:
|
||||
call(UpdateStatus.PREPARING_PATCH, f=file)
|
||||
current_game_file = self._game_dir.joinpath(file)
|
||||
if not current_game_file.exists():
|
||||
# Not patching since we don't have the file
|
||||
continue
|
||||
|
||||
patch_file = str(file) + ".hdiff"
|
||||
|
||||
async def extract_and_patch(old_file, diff_file):
|
||||
call(UpdateStatus.PATCHING, f=old_file)
|
||||
diff_path = self._cache_path.joinpath(diff_file)
|
||||
if diff_path.is_file():
|
||||
diff_path.unlink(missing_ok=True)
|
||||
await asyncio.to_thread(archive.extract, diff_file, self._cache_path)
|
||||
patch_path = self._cache_path.joinpath(diff_file)
|
||||
old_suffix = old_file.suffix
|
||||
old_file = await old_file.rename(old_file.with_suffix(".bak"))
|
||||
proc = await self._hdiffpatch.patch_file(
|
||||
old_file, old_file.with_suffix(old_suffix), patch_path, wait=True
|
||||
)
|
||||
patch_path.unlink()
|
||||
if proc.returncode != 0:
|
||||
# Let the game download the file.
|
||||
await old_file.rename(old_file.with_suffix(old_suffix))
|
||||
return
|
||||
await old_file.unlink()
|
||||
|
||||
files.remove(patch_file)
|
||||
patch_jobs.append(extract_and_patch(current_game_file, patch_file))
|
||||
|
||||
await asyncio.gather(*patch_jobs)
|
||||
call(UpdateStatus.PREPARING_REMOVE_UNUSED)
|
||||
deletefiles = archive.read("deletefiles.txt").decode().split("\n")
|
||||
for file in deletefiles:
|
||||
current_game_file = Path(self._game_dir.joinpath(file))
|
||||
if not current_game_file.exists():
|
||||
continue
|
||||
if current_game_file.is_file():
|
||||
call(UpdateStatus.REMOVING_UNUSED, f=file)
|
||||
current_game_file.unlink(missing_ok=True)
|
||||
|
||||
call(UpdateStatus.EXTRACTING)
|
||||
await asyncio.to_thread(archive.extractall, self._game_dir, members=files)
|
||||
archive.close()
|
||||
# Update game version on local variable.
|
||||
self._version = self.get_current_game_version()
|
||||
self.set_version_config()
|
||||
call(UpdateStatus.COMPLETED)
|
||||
|
||||
def set_version_config(self, version: str = None):
|
||||
"""
|
||||
Sets the version in config.ini
|
||||
|
||||
Args:
|
||||
version: Version to be set, if not specified then it'll use the current game version
|
||||
"""
|
||||
if not version:
|
||||
version = self.version
|
||||
self._game_config.set_game_version(version)
|
||||
self._game_config.save()
|
||||
|
||||
def _extract_game_file(self, archive: str | bytes | PathLike):
|
||||
if not isinstance(archive, Path):
|
||||
archive = Path(archive).resolve()
|
||||
if not archive.exists():
|
||||
raise FileNotFoundError(f"'{archive}' not found")
|
||||
with zipfile.ZipFile(archive, "r") as f:
|
||||
f.extractall(path=self._game_dir)
|
||||
|
||||
def apply_voicepack(self, archive: str | bytes | PathLike):
|
||||
"""
|
||||
Install or update a voicepack to the game
|
||||
|
||||
Args:
|
||||
archive: The voicepack to install or update
|
||||
"""
|
||||
# Since voicepack packages are unclear about diff package or full package
|
||||
# we will try to extract the voicepack package and apply it to the game
|
||||
# making this function universal for both cases
|
||||
if not self.get_game_data_path().exists():
|
||||
raise FileNotFoundError(f"Game not found in {self._game_dir}")
|
||||
self._extract_game_file(archive)
|
||||
|
||||
async def install_game(
|
||||
self,
|
||||
game_archive: str | bytes | PathLike,
|
||||
force_reinstall: bool = False,
|
||||
callback=None,
|
||||
):
|
||||
"""Installs the game to the current directory
|
||||
|
||||
If `force_reinstall` is True, the game will be uninstalled then reinstalled.
|
||||
"""
|
||||
|
||||
def call(status):
|
||||
if not callback:
|
||||
return
|
||||
callback(status=status)
|
||||
|
||||
if self.get_game_data_path().exists():
|
||||
if not force_reinstall:
|
||||
raise ValueError(f"Game is already installed in {self._game_dir}")
|
||||
call(InstallStatus.REMOVING_OLD_GAME)
|
||||
self.delete_game()
|
||||
|
||||
self._game_dir.mkdir(parents=True, exist_ok=True)
|
||||
call(InstallStatus.EXTRACTING)
|
||||
self._extract_game_file(game_archive)
|
||||
self._version = self.get_current_game_version()
|
||||
self.set_version_config()
|
||||
call(InstallStatus.COMPLETED)
|
||||
|
||||
async def get_voicepack_diff_archive(
|
||||
self, lang: str, from_version: str = None, pre_download=False
|
||||
) -> installer.Diff:
|
||||
"""Gets a diff archive from `from_version` to the latest one
|
||||
|
||||
If from_version is not specified, it will be taken from the current game version.
|
||||
"""
|
||||
from_version = from_version if from_version else self.version
|
||||
game = await self.get_server_game_info(pre_download=pre_download)
|
||||
translated_lang = voicepack_lang_translate(lang)
|
||||
for v in game.diffs:
|
||||
if v.version != from_version:
|
||||
continue
|
||||
for vo in v.voice_packs:
|
||||
if vo.language == translated_lang:
|
||||
return vo
|
||||
|
||||
async def get_game_diff_archive(
|
||||
self, from_version: str = None, pre_download=False
|
||||
) -> installer.Diff:
|
||||
"""Gets a diff archive from `from_version` to the latest one
|
||||
|
||||
If from_version is not specified, it will be taken from the current game version.
|
||||
"""
|
||||
from_version = from_version if from_version else self.version
|
||||
game = await self.get_server_game_info(pre_download=pre_download)
|
||||
for v in game.diffs:
|
||||
if v.version == from_version:
|
||||
return v
|
||||
|
||||
def verify_game_file(
|
||||
self,
|
||||
file: str | bytes | PathLike,
|
||||
pkg_version: dict[str, str] | str | bytes | PathLike = None,
|
||||
ignore_mismatch=True,
|
||||
):
|
||||
"""
|
||||
Verifies a game file
|
||||
|
||||
It is strongly recommended to parse pkg_version with parse_pkg_version and use
|
||||
that as pkg_version argument if you're planning to verify multiple game files
|
||||
to save resource and verify faster.
|
||||
|
||||
Args:
|
||||
file: The file to verify (e.g. GenshinImpact_Data/sharedassets0.assets.resS)
|
||||
pkg_version: The pkg_version file to read MD5 from, if not specified it'll use
|
||||
the file from the current game installation
|
||||
ignore_mismatch: Do not raise exception if a file has mismatch MD5, instead
|
||||
return as a (file, expected md5, actual md5) tuple
|
||||
Returns:
|
||||
None if the file is good, (file, expected md5, actual md5) if the file has mismatch md5
|
||||
Raises:
|
||||
FileNotFoundError if the file to verify doesn't exist
|
||||
"""
|
||||
if pkg_version is None:
|
||||
pkg_version = self._game_dir.joinpath("pkg_version")
|
||||
return verify_game_file(
|
||||
file=file,
|
||||
game_dir=self._game_dir,
|
||||
pkg_version=pkg_version,
|
||||
ignore_mismatch=ignore_mismatch,
|
||||
)
|
||||
|
||||
async def verify_game_files(
|
||||
self,
|
||||
pkg_version: dict[str, str] | str | bytes | PathLike = None,
|
||||
ignore_mismatch=True,
|
||||
):
|
||||
"""
|
||||
Verifies the current game installation
|
||||
|
||||
Args:
|
||||
pkg_version: The pkg_version file to read MD5 from, if not specified it'll use
|
||||
the file from the current game installation
|
||||
ignore_mismatch: Do not raise exception if a file has mismatch MD5, instead
|
||||
return all of them as a list with (file, expected md5, actual md5)
|
||||
Returns:
|
||||
None if all files are good, a list with (file, expected md5, actual md5) for mismatch files
|
||||
"""
|
||||
if pkg_version is None:
|
||||
pkg_version = self._game_dir.joinpath("pkg_version")
|
||||
return await verify_game_files(
|
||||
game_dir=self._game_dir,
|
||||
pkg_version=pkg_version,
|
||||
ignore_mismatch=ignore_mismatch,
|
||||
)
|
||||
|
||||
def delete_game(self):
|
||||
"""
|
||||
Deletes the current game installation
|
||||
"""
|
||||
shutil.rmtree(self._game_dir, ignore_errors=True)
|
||||
self._version = None
|
||||
self._variant = None
|
||||
|
||||
def clear_cache(self):
|
||||
"""
|
||||
Clears the cache (e.g. downloaded game files)
|
||||
"""
|
||||
shutil.rmtree(self._cache_path, ignore_errors=True)
|
|
@ -0,0 +1,40 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class Variant(Enum):
|
||||
INTERNATIONAL = "international"
|
||||
CHINESE = "chinese"
|
||||
BILIBILI = "bilibili"
|
||||
|
||||
|
||||
class VersionLocation(Enum):
|
||||
GAME_FILE = "globalgamemanagers"
|
||||
LAUNCHER_CONFIG = "config.ini"
|
||||
|
||||
|
||||
class VoicepackArchiveType(Enum):
|
||||
FULL = 0
|
||||
UPDATE = 1
|
||||
|
||||
|
||||
class VoicepackArchiveLanguage(Enum):
|
||||
English = "English(US)"
|
||||
Japanese = "Japanese"
|
||||
Korean = "Korean"
|
||||
Chinese = "Chinese"
|
||||
|
||||
|
||||
class InstallStatus(Enum):
|
||||
REMOVING_OLD_GAME = 0
|
||||
EXTRACTING = 1
|
||||
COMPLETED = 2
|
||||
|
||||
|
||||
class UpdateStatus(Enum):
|
||||
DOWNLOADING_PATCHER = 0
|
||||
PREPARING_PATCH = 1
|
||||
PATCHING = 2
|
||||
PREPARING_REMOVE_UNUSED = 3
|
||||
REMOVING_UNUSED = 4
|
||||
EXTRACTING = 5
|
||||
COMPLETED = 6
|
|
@ -0,0 +1,103 @@
|
|||
from pathlib import Path
|
||||
|
||||
from worthless import helper
|
||||
from worthless.game import Game
|
||||
|
||||
|
||||
class Helper:
|
||||
"""
|
||||
Quick and dirty extra functions for Game
|
||||
|
||||
Since this is quick and dirty, you are recommended to write your own method instead.
|
||||
|
||||
Args:
|
||||
game: A worthless.game.Game instance
|
||||
"""
|
||||
|
||||
def __init__(self, game: Game):
|
||||
self._download_chunk = 8192
|
||||
self._game = game
|
||||
|
||||
async def _download_file(
|
||||
self, file_url: str, file_name: str, file_len: int = None, overwrite=False
|
||||
) -> Path:
|
||||
"""
|
||||
Download file name to temporary directory.
|
||||
|
||||
This function is a wrapper for helper.download_file
|
||||
|
||||
Args:
|
||||
file_url: File url to download
|
||||
file_name: File name to download into
|
||||
Returns:
|
||||
A Path object containing downloaded file
|
||||
"""
|
||||
return await helper.download_file(
|
||||
file_url,
|
||||
file_name,
|
||||
self._game.cache_path,
|
||||
file_len=file_len,
|
||||
overwrite=overwrite,
|
||||
chunks=self._download_chunk,
|
||||
)
|
||||
|
||||
@property
|
||||
def download_chunk(self):
|
||||
return self._download_chunk
|
||||
|
||||
@download_chunk.setter
|
||||
def download_chunk(self, chunk: int) -> None:
|
||||
"""
|
||||
Sets the download chunk for the internal download function
|
||||
|
||||
Args:
|
||||
chunk: Chunk to set into
|
||||
"""
|
||||
self._download_chunk = chunk
|
||||
|
||||
async def download_full_game(self, pre_download: bool = False) -> Path:
|
||||
game = await self._game.get_server_game_info(pre_download)
|
||||
archive_name = game.latest.path.split("/")[-1]
|
||||
return await self._download_file(
|
||||
game.latest.path, archive_name, game.latest.size
|
||||
)
|
||||
|
||||
async def download_full_voicepack(
|
||||
self, language: str, pre_download: bool = False
|
||||
) -> Path:
|
||||
game = await self._game.get_server_game_info(pre_download)
|
||||
translated_lang = self._game.voicepack_lang_translate(language)
|
||||
for vo in game.latest.voice_packs:
|
||||
if vo.language == translated_lang:
|
||||
return await self._download_file(vo.path, vo.get_name(), vo.size)
|
||||
|
||||
async def download_game_update(
|
||||
self, from_version: str = None, pre_download: bool = False
|
||||
) -> Path:
|
||||
from_version = from_version if from_version else self._game.version
|
||||
game = await self._game.get_server_game_info(pre_download=pre_download)
|
||||
if self._game.version == game.latest.version:
|
||||
raise ValueError("Game is already up to date.")
|
||||
diff_archive = await self._game.get_game_diff_archive(
|
||||
from_version, pre_download
|
||||
)
|
||||
if diff_archive is None:
|
||||
raise ValueError(
|
||||
"Game diff archive is not available for this version, please reinstall."
|
||||
)
|
||||
return await self._download_file(
|
||||
diff_archive.path, diff_archive.name, diff_archive.size
|
||||
)
|
||||
|
||||
async def download_voicepack_update(
|
||||
self, language: str, from_version: str = None, pre_download: bool = False
|
||||
) -> Path:
|
||||
from_version = from_version if from_version else self._game.version
|
||||
diff_archive = await self._game.get_voicepack_diff_archive(
|
||||
language, from_version, pre_download
|
||||
)
|
||||
if diff_archive is None:
|
||||
raise ValueError("Voiceover diff archive is not available for this version")
|
||||
return await self._download_file(
|
||||
diff_archive.path, diff_archive.name, diff_archive.size
|
||||
)
|
|
@ -0,0 +1,132 @@
|
|||
import platform
|
||||
import shutil
|
||||
import asyncio
|
||||
import zipfile
|
||||
|
||||
import aiohttp
|
||||
from pathlib import Path
|
||||
from worthless import constants, helper
|
||||
|
||||
|
||||
class HDiffPatch:
|
||||
"""
|
||||
Contains legacy HDiffPatch support for worthless
|
||||
|
||||
You should not use this class directly, since it's automatically used by Game when needed
|
||||
"""
|
||||
|
||||
def __init__(self, git_url=None, data_dir=None):
|
||||
if not git_url:
|
||||
git_url = constants.HDIFFPATCH_GIT_URL
|
||||
self._git_url = git_url
|
||||
if not data_dir:
|
||||
self._appdirs = constants.APPDIRS
|
||||
self.temp_path = Path(self._appdirs.user_cache_dir).joinpath("HDiffPatch")
|
||||
self.data_path = Path(self._appdirs.user_data_dir).joinpath(
|
||||
"Tools/HDiffPatch"
|
||||
)
|
||||
else:
|
||||
if not isinstance(data_dir, Path):
|
||||
data_dir = Path(data_dir)
|
||||
self.data_path = Path(data_dir).joinpath("Tools/HDiffPatch")
|
||||
self.temp_path = data_dir.joinpath("Temp/HDiffPatch")
|
||||
self.temp_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@staticmethod
|
||||
def _get_platform_arch():
|
||||
match platform.system():
|
||||
case "Windows":
|
||||
match platform.architecture()[0]:
|
||||
case "32bit":
|
||||
return "windows32"
|
||||
case "64bit":
|
||||
return "windows64"
|
||||
case "Linux":
|
||||
match platform.architecture()[0]:
|
||||
case "32bit":
|
||||
return "linux32"
|
||||
case "64bit":
|
||||
return "linux64"
|
||||
case "Darwin":
|
||||
return "macos"
|
||||
|
||||
raise RuntimeError("Unsupported platform")
|
||||
|
||||
def _get_hdiffpatch_exec(self, exec_name):
|
||||
if shutil.which(exec_name):
|
||||
return exec_name
|
||||
if not self.data_path.exists():
|
||||
return None
|
||||
if not any(self.data_path.iterdir()):
|
||||
return None
|
||||
platform_arch_path = self.data_path.joinpath(self._get_platform_arch())
|
||||
file = platform_arch_path.joinpath(exec_name)
|
||||
if file.exists():
|
||||
file.chmod(0o755)
|
||||
return str(file)
|
||||
return None
|
||||
|
||||
def get_hpatchz_executable(self):
|
||||
hpatchz_name = "hpatchz" + (".exe" if platform.system() == "Windows" else "")
|
||||
return self._get_hdiffpatch_exec(hpatchz_name)
|
||||
|
||||
async def patch_file(self, in_file, out_file, patch_file, error=False, wait=False):
|
||||
hpatchz = self.get_hpatchz_executable()
|
||||
if not hpatchz:
|
||||
raise RuntimeError("hpatchz executable not found")
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
hpatchz, "-f", in_file, patch_file, out_file
|
||||
)
|
||||
if not wait:
|
||||
return proc
|
||||
await proc.wait()
|
||||
if error and proc.returncode != 0:
|
||||
raise RuntimeError(f"Patching failed, return code is {proc.returncode}")
|
||||
return proc
|
||||
|
||||
def get_hdiffz_executable(self):
|
||||
hdiffz_name = "hdiffz" + (".exe" if platform.system() == "Windows" else "")
|
||||
return self._get_hdiffpatch_exec(hdiffz_name)
|
||||
|
||||
async def _get_latest_release_info(self):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
split = self._git_url.split("/")
|
||||
repo = split[-1]
|
||||
owner = split[-2]
|
||||
rsp = await session.get(
|
||||
"https://api.github.com/repos/{}/{}/releases/latest".format(
|
||||
owner, repo
|
||||
),
|
||||
params={"Headers": "Accept: application/vnd.github.v3+json"},
|
||||
)
|
||||
rsp.raise_for_status()
|
||||
for asset in (await rsp.json())["assets"]:
|
||||
if (
|
||||
asset["name"].endswith(".zip")
|
||||
and "linux" not in asset["name"]
|
||||
and "windows" not in asset["name"]
|
||||
and "macos" not in asset["name"]
|
||||
and "android" not in asset["name"]
|
||||
):
|
||||
return asset
|
||||
|
||||
async def get_latest_release_url(self):
|
||||
asset = await self._get_latest_release_info()
|
||||
return asset["browser_download_url"]
|
||||
|
||||
async def get_latest_release_name(self):
|
||||
asset = await self._get_latest_release_info()
|
||||
return asset["name"]
|
||||
|
||||
async def download_latest_release(self, extract=True):
|
||||
url = await self.get_latest_release_url()
|
||||
name = await self.get_latest_release_name()
|
||||
if not url:
|
||||
raise RuntimeError("Unable to find latest release")
|
||||
await helper.download_file(
|
||||
url, name, self.temp_path, overwrite=True, threads_num=1
|
||||
)
|
||||
if not extract:
|
||||
return
|
||||
with zipfile.ZipFile(self.temp_path.joinpath(name), "r") as f:
|
||||
await asyncio.to_thread(f.extractall, path=self.data_path)
|
|
@ -0,0 +1,83 @@
|
|||
from configparser import ConfigParser
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from worthless.game.gameenums import Variant
|
||||
|
||||
|
||||
class LauncherConfig:
|
||||
"""
|
||||
Provides config.ini for official launcher compatibility
|
||||
|
||||
You should not use this class directly, since it's automatically used by Game when needed
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create_config(game_version, variant: Variant):
|
||||
"""
|
||||
Creates `config.ini`
|
||||
https://notabug.org/Krock/dawn/src/master/updater/update_gi.sh#L212
|
||||
"""
|
||||
sub_channel = 0
|
||||
channel = 1
|
||||
cps = "mihoyo"
|
||||
match variant:
|
||||
case Variant.INTERNATIONAL:
|
||||
channel = 1
|
||||
sub_channel = 0
|
||||
cps = "mihoyo"
|
||||
case Variant.CHINESE:
|
||||
channel = 1
|
||||
sub_channel = 1
|
||||
cps = "mihoyo"
|
||||
case Variant.BILIBILI:
|
||||
channel = 14
|
||||
sub_channel = 0
|
||||
cps = "bilibili"
|
||||
config = ConfigParser()
|
||||
config.add_section("General")
|
||||
config.set("General", "channel", str(channel))
|
||||
config.set("General", "cps", cps)
|
||||
config.set("General", "game_version", game_version)
|
||||
config.set("General", "sdk_version", "")
|
||||
config.set("General", "sub_channel", str(sub_channel))
|
||||
return config
|
||||
|
||||
def __init__(
|
||||
self, config_path: PathLike, game_version=None, variant: Variant = None
|
||||
):
|
||||
if not variant:
|
||||
variant = Variant.INTERNATIONAL
|
||||
if not isinstance(config_path, Path):
|
||||
config_path = Path(config_path)
|
||||
if not game_version:
|
||||
game_version = "0.0.0"
|
||||
self._config_path = config_path
|
||||
self._config = ConfigParser()
|
||||
if self._config_path.exists():
|
||||
self._config.read(self._config_path)
|
||||
else:
|
||||
self._config = self.create_config(game_version, variant)
|
||||
|
||||
def set_game_version(self, game_version):
|
||||
self._config.set("General", "game_version", game_version)
|
||||
|
||||
def set_variant(self, variant: Variant):
|
||||
sub_channel = 0
|
||||
match variant:
|
||||
case Variant.INTERNATIONAL:
|
||||
sub_channel = 0
|
||||
case Variant.CHINESE:
|
||||
sub_channel = 1
|
||||
case Variant.BILIBILI:
|
||||
sub_channel = 0
|
||||
self._config.set("General", "sub_channel", str(sub_channel))
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Saves config.ini
|
||||
"""
|
||||
with self._config_path.open("w") as config_file:
|
||||
self._config.write(config_file)
|
||||
|
||||
def get(self, section: str, option: str, **kwargs) -> str:
|
||||
return self._config.get(section=section, option=option, **kwargs)
|
|
@ -0,0 +1,104 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import math
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
import aiohttp
|
||||
|
||||
_logger = logging.getLogger("worthless.helper")
|
||||
|
||||
|
||||
async def download_file(
|
||||
file_url: str,
|
||||
file_name: str,
|
||||
file_path: Path | str,
|
||||
file_len: int = None,
|
||||
overwrite: bool = False,
|
||||
chunks: int = None,
|
||||
threads_num: int = None,
|
||||
) -> Path:
|
||||
"""
|
||||
Download file name to file_path.
|
||||
|
||||
You should implement your own download method instead of using this.
|
||||
|
||||
Args:
|
||||
file_url: Url to download the file from
|
||||
file_name: File name to download into
|
||||
file_path: Path to download file into
|
||||
file_len: File length to support threaded downloading
|
||||
overwrite: Whether overwrite existing file or not
|
||||
chunks: Chunks to write file into memory before writing to disk
|
||||
threads_num: Number of download threads
|
||||
Return:
|
||||
Downloaded file as a Path object
|
||||
"""
|
||||
logger = _logger.getChild("download_file")
|
||||
if not chunks:
|
||||
chunks = 8192
|
||||
if not threads_num:
|
||||
threads_num = 8
|
||||
logger.debug("Downloading chunks {} with {} thread".format(chunks, threads_num))
|
||||
file_path = Path(file_path).joinpath(file_name)
|
||||
|
||||
async def _download(
|
||||
session: aiohttp.ClientSession,
|
||||
from_bytes: int,
|
||||
to_bytes: int,
|
||||
threaded: bool = None,
|
||||
) -> Path:
|
||||
headers = {"Range": f"bytes={from_bytes}-{to_bytes if to_bytes else ''}"}
|
||||
if threaded:
|
||||
p = file_path.parent.joinpath(secrets.token_urlsafe(16))
|
||||
else:
|
||||
p = file_path
|
||||
p.touch(exist_ok=True)
|
||||
rsp = await session.get(file_url, headers=headers, timeout=None)
|
||||
if rsp.status == 416:
|
||||
# Not an error, so yeah.
|
||||
return p
|
||||
rsp.raise_for_status()
|
||||
with p.open("ab") as file:
|
||||
async for chunk in rsp.content.iter_chunked(chunks):
|
||||
await asyncio.to_thread(file.write, chunk)
|
||||
|
||||
return p
|
||||
|
||||
if overwrite:
|
||||
file_path.unlink(missing_ok=True)
|
||||
if file_path.exists():
|
||||
cur_len = file_path.stat().st_size
|
||||
else:
|
||||
file_path.touch()
|
||||
cur_len = 0
|
||||
if not file_len or threads_num == 1:
|
||||
async with aiohttp.ClientSession() as s:
|
||||
return await _download(session=s, from_bytes=cur_len, to_bytes=file_len)
|
||||
|
||||
download_bytes = file_len - cur_len
|
||||
# if bytes * threads is smaller than file_len then we will not get the full file.
|
||||
download_bytes_t = math.ceil(download_bytes / threads_num)
|
||||
download_jobs = []
|
||||
current_bytes = cur_len
|
||||
async with aiohttp.ClientSession() as s:
|
||||
for thread in range(threads_num):
|
||||
next_bytes = current_bytes + download_bytes_t
|
||||
if next_bytes > file_len:
|
||||
next_bytes = file_len
|
||||
download_jobs.append(
|
||||
_download(
|
||||
session=s,
|
||||
from_bytes=current_bytes,
|
||||
to_bytes=next_bytes,
|
||||
threaded=True,
|
||||
)
|
||||
)
|
||||
# Move to next bytes
|
||||
current_bytes = next_bytes
|
||||
all_bytes = await asyncio.gather(*download_jobs)
|
||||
# Merge bytes into the file
|
||||
with file_path.open("ab") as f:
|
||||
for bytes_path in all_bytes:
|
||||
f.write(bytes_path.read_bytes())
|
||||
bytes_path.unlink()
|
|
@ -1,636 +0,0 @@
|
|||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import zipfile
|
||||
from configparser import ConfigParser
|
||||
from pathlib import Path
|
||||
|
||||
import aiohttp
|
||||
|
||||
from worthless import constants
|
||||
from worthless.launcher import Launcher
|
||||
from worthless.launcherconfig import LauncherConfig
|
||||
|
||||
|
||||
async def _download_file(file_url: str, file_name: str, file_path: Path | str, file_len: int = None, overwrite=False,
|
||||
chunks=8192):
|
||||
"""
|
||||
Download file name to temporary directory,
|
||||
:param file_url:
|
||||
:param file_name:
|
||||
:return:
|
||||
"""
|
||||
headers = {}
|
||||
file_path = Path(file_path).joinpath(file_name)
|
||||
if overwrite:
|
||||
file_path.unlink(missing_ok=True)
|
||||
if file_path.exists():
|
||||
cur_len = (file_path.stat()).st_size
|
||||
headers |= {
|
||||
"Range": f"bytes={cur_len}-{file_len if file_len else ''}"
|
||||
}
|
||||
else:
|
||||
file_path.touch()
|
||||
print(f"Downloading {file_url} to {file_path}...")
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=60*60, sock_read=240)) as session:
|
||||
rsp = await session.get(file_url, headers=headers, timeout=None)
|
||||
if rsp.status == 416:
|
||||
return
|
||||
rsp.raise_for_status()
|
||||
while True:
|
||||
chunk = await rsp.content.read(chunks)
|
||||
await asyncio.sleep(0)
|
||||
if not chunk:
|
||||
break
|
||||
with file_path.open("ab") as f:
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
def calculate_md5(file_to_calculate):
|
||||
file_to_calculate = Path(file_to_calculate)
|
||||
if not file_to_calculate.exists():
|
||||
return ""
|
||||
with file_to_calculate.open("rb") as f:
|
||||
file_hash = hashlib.md5()
|
||||
while chunk := f.read(1024 * 1024):
|
||||
file_hash.update(chunk)
|
||||
return file_hash.hexdigest()
|
||||
|
||||
|
||||
class HDiffPatch:
|
||||
def __init__(self, git_url=None, data_dir=None):
|
||||
if not git_url:
|
||||
git_url = constants.HDIFFPATCH_GIT_URL
|
||||
self._git_url = git_url
|
||||
if not data_dir:
|
||||
self._appdirs = constants.APPDIRS
|
||||
self.temp_path = Path(self._appdirs.user_cache_dir).joinpath("HDiffPatch")
|
||||
self.data_path = Path(self._appdirs.user_data_dir).joinpath("Tools/HDiffPatch")
|
||||
else:
|
||||
if not isinstance(data_dir, Path):
|
||||
data_dir = Path(data_dir)
|
||||
self.data_path = Path(data_dir).joinpath("Tools/HDiffPatch")
|
||||
self.temp_path = data_dir.joinpath("Temp/HDiffPatch")
|
||||
Path(self.temp_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@staticmethod
|
||||
def _get_platform_arch():
|
||||
processor = platform.machine()
|
||||
match platform.system():
|
||||
case "Windows":
|
||||
match processor:
|
||||
case "i386":
|
||||
return "windows32"
|
||||
case "x86_64":
|
||||
return "windows64"
|
||||
case "arm":
|
||||
return "windows_arm32"
|
||||
case "arm64":
|
||||
return "windows_arm64"
|
||||
case "Linux":
|
||||
match processor:
|
||||
case "i386":
|
||||
return "linux32"
|
||||
case "x86_64":
|
||||
return "linux64"
|
||||
case "arm":
|
||||
return "linux_arm32"
|
||||
case "arm64":
|
||||
return "linux_arm64"
|
||||
case "Darwin":
|
||||
return "macos"
|
||||
|
||||
# Rip BSD they need to use Linux compatibility layer to run this (or use Wine if they prefer that)
|
||||
raise RuntimeError("Unsupported platform")
|
||||
|
||||
def _get_hdiffpatch_exec(self, exec_name):
|
||||
if shutil.which(exec_name):
|
||||
return exec_name
|
||||
if not self.data_path.exists():
|
||||
return None
|
||||
if not any(self.data_path.iterdir()):
|
||||
return None
|
||||
platform_arch_path = self.data_path.joinpath(self._get_platform_arch())
|
||||
file = platform_arch_path.joinpath(exec_name)
|
||||
if file.exists():
|
||||
file.chmod(0o755)
|
||||
return str(file)
|
||||
return None
|
||||
|
||||
def get_hpatchz_executable(self):
|
||||
hpatchz_name = "hpatchz" + (".exe" if platform.system() == "Windows" else "")
|
||||
return self._get_hdiffpatch_exec(hpatchz_name)
|
||||
|
||||
async def patch_file(self, in_file, out_file, patch_file, error=False, wait=False):
|
||||
print("executing hpatchz")
|
||||
hpatchz = self.get_hpatchz_executable()
|
||||
if not hpatchz:
|
||||
raise RuntimeError("hpatchz executable not found")
|
||||
proc = await asyncio.create_subprocess_exec(hpatchz, "-f", in_file, patch_file, out_file)
|
||||
if not wait:
|
||||
return proc
|
||||
await proc.wait()
|
||||
if error and proc.returncode != 0:
|
||||
raise RuntimeError(f"Patching failed, return code is {proc.returncode}")
|
||||
return proc
|
||||
|
||||
def get_hdiffz_executable(self):
|
||||
hdiffz_name = "hdiffz" + (".exe" if platform.system() == "Windows" else "")
|
||||
return self._get_hdiffpatch_exec(hdiffz_name)
|
||||
|
||||
async def _get_latest_release_info(self):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
split = self._git_url.split("/")
|
||||
repo = split[-1]
|
||||
owner = split[-2]
|
||||
rsp = await session.get("https://api.github.com/repos/{}/{}/releases/latest".format(owner, repo),
|
||||
params={"Headers": "Accept: application/vnd.github.v3+json"})
|
||||
rsp.raise_for_status()
|
||||
archive_processor = self._get_platform_arch()
|
||||
for asset in (await rsp.json())["assets"]:
|
||||
if not asset["name"].endswith(".zip"):
|
||||
continue
|
||||
if archive_processor not in asset["name"]:
|
||||
continue
|
||||
return asset
|
||||
|
||||
async def get_latest_release_url(self):
|
||||
asset = await self._get_latest_release_info()
|
||||
return asset["browser_download_url"]
|
||||
|
||||
async def get_latest_release_name(self):
|
||||
asset = await self._get_latest_release_info()
|
||||
return asset["name"]
|
||||
|
||||
async def download_latest_release(self, extract=True):
|
||||
url = await self.get_latest_release_url()
|
||||
name = await self.get_latest_release_name()
|
||||
if not url:
|
||||
raise RuntimeError("Unable to find latest release")
|
||||
await _download_file(url, name, self.temp_path, overwrite=True)
|
||||
if not extract:
|
||||
return
|
||||
with zipfile.ZipFile(self.temp_path.joinpath(name), 'r') as f:
|
||||
await asyncio.to_thread(f.extractall, path=self.data_path)
|
||||
|
||||
|
||||
class Installer:
|
||||
def __init__(self, gamedir: str | Path = Path.cwd(),
|
||||
overseas: bool = True, data_dir: str | Path = None):
|
||||
if isinstance(gamedir, str | Path):
|
||||
gamedir = Path(gamedir)
|
||||
self._gamedir = gamedir
|
||||
if not data_dir:
|
||||
self._appdirs = constants.APPDIRS
|
||||
self.temp_path = Path(self._appdirs.user_cache_dir).joinpath("Installer")
|
||||
else:
|
||||
if isinstance(data_dir, str | Path):
|
||||
data_dir = Path(data_dir)
|
||||
self.temp_path = data_dir.joinpath("Temp/Installer/")
|
||||
Path(self.temp_path).mkdir(parents=True, exist_ok=True)
|
||||
config_file = self._gamedir.joinpath("config.ini")
|
||||
self._config_file = config_file
|
||||
self._download_chunk = 1024 * 1024
|
||||
self._overseas = overseas
|
||||
self._version = None
|
||||
self._launcher = Launcher(self._gamedir, overseas=self._overseas)
|
||||
self._hdiffpatch = HDiffPatch(data_dir=data_dir)
|
||||
self._config = LauncherConfig(self._config_file, self._version)
|
||||
self._game_version_re = re.compile(r"([1-9]+\.[0-9]+\.[0-9]+)_\d+_\d+")
|
||||
|
||||
async def _download_file(self, file_url: str, file_name: str, file_len: int = None, overwrite=False):
|
||||
"""
|
||||
Download file name to temporary directory,
|
||||
:param file_url:
|
||||
:param file_name:
|
||||
:return:
|
||||
"""
|
||||
await _download_file(file_url, file_name, self.temp_path, file_len=file_len, overwrite=overwrite,
|
||||
chunks=self._download_chunk)
|
||||
|
||||
async def read_version_from_config(self):
|
||||
if not self._config_file.exists():
|
||||
raise FileNotFoundError(f"Config file {self._config_file} not found")
|
||||
cfg = ConfigParser()
|
||||
await asyncio.to_thread(cfg.read, str(self._config_file))
|
||||
return cfg.get("General", "game_version")
|
||||
|
||||
async def read_version_from_game_file(self, globalgamemanagers: Path | Path | bytes) -> str:
|
||||
"""
|
||||
Reads the version from the globalgamemanagers file. (Data/globalgamemanagers)
|
||||
|
||||
Uses `An Anime Game Launcher` method to read the version:
|
||||
https://gitlab.com/KRypt0n_/an-anime-game-launcher/-/blob/main/src/ts/Game.ts#L26
|
||||
|
||||
:return: Game version (ex 1.0.0)
|
||||
"""
|
||||
if isinstance(globalgamemanagers, Path | Path):
|
||||
globalgamemanagers = Path(globalgamemanagers)
|
||||
data = globalgamemanagers.read_text("ascii", errors="ignore")
|
||||
else:
|
||||
data = globalgamemanagers.decode("ascii", errors="ignore")
|
||||
result = self._game_version_re.search(data)
|
||||
if not result:
|
||||
raise ValueError("Could not find version in game file")
|
||||
return result.group(1)
|
||||
|
||||
@staticmethod
|
||||
def voiceover_lang_translate(lang: str, base_language="game") -> str:
|
||||
"""
|
||||
Translates the voiceover language to the language code used by the game.
|
||||
:param lang: Language to translate
|
||||
:param base_language: Base language type (game/locale/both)
|
||||
:return: Language code
|
||||
"""
|
||||
if base_language == "game" or base_language == "both":
|
||||
match lang.lower():
|
||||
case "english(us)":
|
||||
return "en-us"
|
||||
case "japanese":
|
||||
return "ja-jp"
|
||||
case "chinese":
|
||||
return "zh-cn"
|
||||
case "korean":
|
||||
return "ko-kr"
|
||||
if base_language == "locale" or base_language == "both":
|
||||
match lang.lower().replace("_", "-"):
|
||||
case "en-us":
|
||||
return "English(US)"
|
||||
case "ja-jp":
|
||||
return "Japanese"
|
||||
case "zh-cn":
|
||||
return "Chinese"
|
||||
case "ko-kr":
|
||||
return "Korean"
|
||||
# If nothing else matches
|
||||
return lang
|
||||
|
||||
@staticmethod
|
||||
async def get_voiceover_archive_language(voiceover_archive: str | Path | Path) -> str:
|
||||
if isinstance(voiceover_archive, str | Path):
|
||||
voiceover_archive = Path(voiceover_archive).resolve()
|
||||
if not voiceover_archive.exists():
|
||||
raise FileNotFoundError(f"Voiceover archive {voiceover_archive} not found")
|
||||
with zipfile.ZipFile(voiceover_archive, 'r') as f:
|
||||
for file in zipfile.Path(f).iterdir():
|
||||
if file.name.endswith("_pkg_version"):
|
||||
return file.name.split("_")[1]
|
||||
|
||||
@staticmethod
|
||||
async def get_voiceover_archive_type(voiceover_archive: str | Path) -> bool:
|
||||
"""
|
||||
Gets voiceover archive type.
|
||||
:param voiceover_archive:
|
||||
:return: True if this is a full archive, else False.
|
||||
"""
|
||||
vo_lang = await Installer.get_voiceover_archive_language(voiceover_archive)
|
||||
with zipfile.ZipFile(voiceover_archive, 'r') as f:
|
||||
archive_path = zipfile.Path(f)
|
||||
files = (await asyncio.to_thread(f.read, "Audio_{}_pkg_version".format(vo_lang))).decode().split("\n")
|
||||
for file in files:
|
||||
if file.strip() and not archive_path.joinpath(json.loads(file)["remoteName"]).exists():
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_download_chunk(self, chunk: int):
|
||||
self._download_chunk = chunk
|
||||
|
||||
def get_game_data_name(self):
|
||||
if self._overseas:
|
||||
return "GenshinImpact_Data/"
|
||||
else:
|
||||
return "YuanShen_Data/"
|
||||
|
||||
def get_game_data_path(self) -> Path:
|
||||
return self._gamedir.joinpath(self.get_game_data_name())
|
||||
|
||||
async def get_game_archive_version(self, game_archive: str | Path):
|
||||
game_archive = Path(game_archive)
|
||||
if not game_archive.is_file():
|
||||
raise FileNotFoundError(f"Game archive {game_archive} not found")
|
||||
with zipfile.ZipFile(game_archive, 'r') as f:
|
||||
return await self.read_version_from_game_file(
|
||||
await asyncio.to_thread(f.read, self.get_game_data_name() + "globalgamemanagers")
|
||||
)
|
||||
|
||||
async def get_game_version(self) -> str | None:
|
||||
globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers")
|
||||
if not globalgamemanagers.exists():
|
||||
try:
|
||||
return await self.read_version_from_config()
|
||||
except FileNotFoundError:
|
||||
return
|
||||
return await self.read_version_from_game_file(globalgamemanagers)
|
||||
|
||||
async def get_installed_voiceovers(self) -> list[str]:
|
||||
"""
|
||||
Returns a list of installed voiceovers.
|
||||
|
||||
:return: List of installed voiceovers
|
||||
"""
|
||||
voiceovers = []
|
||||
for file in self.get_game_data_path()\
|
||||
.joinpath("StreamingAssets/AudioAssets/").iterdir():
|
||||
if file.is_dir():
|
||||
voiceovers.append(file.name)
|
||||
return voiceovers
|
||||
|
||||
async def _update(self, game_archive: str | Path | Path):
|
||||
archive = zipfile.ZipFile(game_archive, 'r')
|
||||
|
||||
if not self._hdiffpatch.get_hpatchz_executable():
|
||||
await self._hdiffpatch.download_latest_release()
|
||||
|
||||
files = archive.namelist()
|
||||
|
||||
# Don't extract these files (they're useless and if the game isn't patched then it'll
|
||||
# raise 31-4xxx error ingame)
|
||||
for file in ["deletefiles.txt", "hdifffiles.txt"]:
|
||||
try:
|
||||
files.remove(file)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
deletefiles = archive.read("deletefiles.txt").decode().split("\r\n")
|
||||
for file in deletefiles:
|
||||
current_game_file = Path(self._gamedir).joinpath(file)
|
||||
if current_game_file == Path(self._gamedir):
|
||||
# Don't delete the game folder
|
||||
print("Game folder detected, not deleting:", current_game_file)
|
||||
continue
|
||||
if not current_game_file.relative_to(Path(self._gamedir)):
|
||||
print("Not deleting (not relative to game):", current_game_file)
|
||||
continue
|
||||
print("Deleting", file)
|
||||
current_game_file.unlink(missing_ok=True)
|
||||
except Exception as e:
|
||||
print(f"Error while reading deletefiles.txt: {e}")
|
||||
|
||||
# hdiffpatch implementation
|
||||
try:
|
||||
hdifffiles = []
|
||||
for x in (await asyncio.to_thread(archive.read, "hdifffiles.txt")).decode().split("\n"):
|
||||
if x:
|
||||
hdifffiles.append(json.loads(x.strip())["remoteName"])
|
||||
patch_jobs = []
|
||||
cur_jobs = []
|
||||
count = 0
|
||||
for file in hdifffiles:
|
||||
current_game_file = self._gamedir.joinpath(file)
|
||||
if not current_game_file.exists():
|
||||
print("File", file, "not found")
|
||||
# Not patching since we don't have the file
|
||||
continue
|
||||
|
||||
patch_file = str(file) + ".hdiff"
|
||||
|
||||
async def extract_and_patch(old_file, diff_file):
|
||||
patch_path = self.temp_path.joinpath(diff_file)
|
||||
patch_path.unlink(missing_ok=True)
|
||||
try:
|
||||
print(diff_file)
|
||||
await asyncio.to_thread(archive.extract, diff_file, self.temp_path)
|
||||
except FileExistsError:
|
||||
print("Failed to extract diff file", diff_file)
|
||||
return
|
||||
old_suffix = old_file.suffix
|
||||
old_file = old_file.rename(old_file.with_suffix(".bak"))
|
||||
proc = await self._hdiffpatch.patch_file(old_file, old_file.with_suffix(old_suffix),
|
||||
patch_path, wait=True)
|
||||
patch_path.unlink()
|
||||
if proc.returncode == 0:
|
||||
old_file.unlink()
|
||||
return
|
||||
# Let the game download the file.
|
||||
print("Failed to patch {}, reverting and let the in-game updater do the job...".format(
|
||||
old_file.with_suffix(old_suffix))
|
||||
)
|
||||
try:
|
||||
old_file.rename(old_file.with_suffix(old_suffix))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
files.remove(patch_file)
|
||||
# Limit to 8 process running so it doesn't hang the PC.
|
||||
if count == 7:
|
||||
print("add job")
|
||||
patch_jobs.append(cur_jobs)
|
||||
cur_jobs = []
|
||||
count = 0
|
||||
cur_jobs.append(extract_and_patch(current_game_file, patch_file))
|
||||
count += 1
|
||||
|
||||
# The last list may have count < 7 and the above code will not add them
|
||||
patch_jobs.append(cur_jobs)
|
||||
for jobs in patch_jobs:
|
||||
print("exec jobs", jobs)
|
||||
await asyncio.gather(*jobs)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error while reading hdifffiles.txt: {e}")
|
||||
|
||||
await asyncio.to_thread(archive.extractall, self._gamedir, members=files)
|
||||
archive.close()
|
||||
|
||||
async def update_game(self, game_archive: str | Path | Path):
|
||||
if not self.get_game_data_path().exists():
|
||||
raise FileNotFoundError(f"Game not found in {self._gamedir}")
|
||||
if isinstance(game_archive, str | Path):
|
||||
game_archive = Path(game_archive).resolve()
|
||||
if not game_archive.exists():
|
||||
raise FileNotFoundError(f"Update archive {game_archive} not found")
|
||||
|
||||
await self._update(game_archive=game_archive)
|
||||
# Update game version on local variable.
|
||||
self._version = await self.get_game_version()
|
||||
self.set_version_config()
|
||||
|
||||
def set_version_config(self, version: str = None):
|
||||
if not version:
|
||||
version = self._version
|
||||
self._config.set_game_version(version)
|
||||
self._config.save()
|
||||
|
||||
async def download_full_game(self, pre_download=False):
|
||||
game = await self._get_game(pre_download)
|
||||
if not game.latest.path == "":
|
||||
archive_name = game.latest.path.split("/")[-1]
|
||||
if calculate_md5(self.temp_path.joinpath(archive_name)) != game.latest.md5:
|
||||
raise RuntimeError("mismatch md5 for downloaded game archive")
|
||||
return
|
||||
# Segment download
|
||||
base_archive = None
|
||||
for i, segment in enumerate(game.latest.segments):
|
||||
archive_name = segment["path"].split("/")[-1]
|
||||
if i == 0:
|
||||
base_archive = archive_name = Path(archive_name).stem # Remove .001
|
||||
if self.temp_path.joinpath(archive_name + ".downloaded").exists():
|
||||
continue
|
||||
await self._download_file(segment["path"], archive_name)
|
||||
if i != 0:
|
||||
with open(self.temp_path.joinpath(base_archive), 'ab') as f:
|
||||
with open(self.temp_path.joinpath(archive_name), 'rb') as f2:
|
||||
f.write(f2.read())
|
||||
self.temp_path.joinpath(archive_name).unlink()
|
||||
self.temp_path.joinpath(archive_name + ".downloaded").touch()
|
||||
|
||||
|
||||
async def download_full_voiceover(self, language: str, pre_download=False):
|
||||
game = await self._get_game(pre_download)
|
||||
translated_lang = self.voiceover_lang_translate(language)
|
||||
for vo in game.latest.voice_packs:
|
||||
if vo.language == translated_lang:
|
||||
await self._download_file(vo.path, vo.get_name(), vo.size)
|
||||
|
||||
async def uninstall_game(self):
|
||||
await asyncio.to_thread(shutil.rmtree, self._gamedir, ignore_errors=True)
|
||||
|
||||
async def _extract_game_file(self, archive: str | Path | Path):
|
||||
if isinstance(archive, str | Path):
|
||||
archive = Path(archive).resolve()
|
||||
if not archive.exists():
|
||||
raise FileNotFoundError(f"'{archive}' not found")
|
||||
with zipfile.ZipFile(archive, 'r') as f:
|
||||
await asyncio.to_thread(f.extractall, path=self._gamedir)
|
||||
|
||||
async def apply_voiceover(self, voiceover_archive: str | Path):
|
||||
# Since Voiceover packages are unclear about diff package or full package
|
||||
# we will try to extract the voiceover package and apply it to the game
|
||||
# making this function universal for both cases
|
||||
if not self.get_game_data_path().exists():
|
||||
raise FileNotFoundError(f"Game not found in {self._gamedir}")
|
||||
if isinstance(voiceover_archive, str | Path):
|
||||
voiceover_archive = Path(voiceover_archive).resolve()
|
||||
await self._update(voiceover_archive)
|
||||
# await self._extract_game_file(voiceover_archive)
|
||||
|
||||
async def install_game(self, game_archive: str | Path | Path, force_reinstall: bool = False):
|
||||
"""Installs the game to the current directory
|
||||
|
||||
If `force_reinstall` is True, the game will be uninstalled then reinstalled.
|
||||
"""
|
||||
if self.get_game_data_path().exists():
|
||||
if not force_reinstall:
|
||||
raise ValueError(f"Game is already installed in {self._gamedir}")
|
||||
await self.uninstall_game()
|
||||
|
||||
self._gamedir.mkdir(parents=True, exist_ok=True)
|
||||
await self._extract_game_file(game_archive)
|
||||
self._version = await self.get_game_version()
|
||||
self.set_version_config()
|
||||
|
||||
async def _get_game_version(self):
|
||||
if self._version:
|
||||
from_version = self._version
|
||||
else:
|
||||
from_version = self._version = await self.get_game_version()
|
||||
return from_version
|
||||
|
||||
async def _get_game_resource(self):
|
||||
game_resource = await self._launcher.get_resource_info()
|
||||
if not game_resource:
|
||||
raise ValueError("Could not fetch game resource")
|
||||
return game_resource
|
||||
|
||||
async def _get_game(self, pre_download=False):
|
||||
game_resource = await self._get_game_resource()
|
||||
game = game_resource.game
|
||||
if pre_download:
|
||||
game = game_resource.pre_download_game
|
||||
return game
|
||||
|
||||
async def download_game_update(self, from_version: str = None, pre_download=False):
|
||||
if not from_version:
|
||||
from_version = await self._get_game_version()
|
||||
game = await self._get_game(pre_download=pre_download)
|
||||
if self._version == game.latest.version:
|
||||
raise ValueError("Game is already up to date.")
|
||||
diff_archive = await self.get_game_diff_archive(from_version, pre_download)
|
||||
if diff_archive is None:
|
||||
raise ValueError("Game diff archive is not available for this version, please reinstall.")
|
||||
await self._download_file(diff_archive.path, diff_archive.name, diff_archive.size)
|
||||
|
||||
async def download_voiceover_update(self, language: str, from_version: str = None, pre_download=False):
|
||||
if not from_version:
|
||||
from_version = await self._get_game_version()
|
||||
diff_archive = await self.get_voiceover_diff_archive(language, from_version, pre_download)
|
||||
if diff_archive is None:
|
||||
raise ValueError("Voiceover diff archive is not available for this version, please reinstall.")
|
||||
await self._download_file(diff_archive.path, diff_archive.name, diff_archive.size)
|
||||
|
||||
async def get_voiceover_diff_archive(self, lang: str, from_version: str = None, pre_download=False):
|
||||
"""Gets a diff archive from `from_version` to the latest one
|
||||
|
||||
If from_version is not specified, it will be taken from the game version.
|
||||
"""
|
||||
if not from_version:
|
||||
from_version = await self._get_game_version()
|
||||
game = await self._get_game(pre_download=pre_download)
|
||||
translated_lang = self.voiceover_lang_translate(lang)
|
||||
for v in game.diffs:
|
||||
if v.version.strip() != from_version.strip():
|
||||
continue
|
||||
for vo in v.voice_packs:
|
||||
if vo.language == translated_lang:
|
||||
return vo
|
||||
|
||||
async def get_game_diff_archive(self, from_version: str = None, pre_download=False):
|
||||
"""Gets a diff archive from `from_version` to the latest one
|
||||
|
||||
If from_version is not specified, it will be taken from the game version.
|
||||
"""
|
||||
if not from_version:
|
||||
from_version = await self._get_game_version()
|
||||
game = await self._get_game(pre_download=pre_download)
|
||||
for v in game.diffs:
|
||||
if v.version == from_version:
|
||||
return v
|
||||
|
||||
async def verify_from_pkg_version(self, pkg_version: Path, ignore_mismatch=False):
|
||||
contents = pkg_version.read_text()
|
||||
|
||||
async def verify_file(file_to_verify, md5):
|
||||
print("Verifying file:", file_to_verify)
|
||||
file_md5 = await asyncio.to_thread(calculate_md5, file_to_verify)
|
||||
if file_md5 == md5:
|
||||
return None
|
||||
if ignore_mismatch:
|
||||
return file_to_verify, md5, file_md5
|
||||
raise ValueError(f"MD5 does not match for {file_to_verify}, expected md5: {md5}, actual md5: {file_md5}")
|
||||
|
||||
verify_jobs = []
|
||||
cur_jobs = []
|
||||
count = 0
|
||||
for content in contents.split("\r\n"):
|
||||
if not content.strip():
|
||||
continue
|
||||
if count >= 7:
|
||||
verify_jobs.append(cur_jobs)
|
||||
cur_jobs = []
|
||||
count = 0
|
||||
info = json.loads(content)
|
||||
cur_jobs.append(verify_file(self._gamedir.joinpath(info["remoteName"]), info["md5"]))
|
||||
count += 1
|
||||
|
||||
verify_jobs.append(cur_jobs)
|
||||
verify_result = []
|
||||
for jobs in verify_jobs:
|
||||
verify_result.extend(await asyncio.gather(*jobs))
|
||||
failed_files = []
|
||||
for file in verify_result:
|
||||
if file is not None:
|
||||
failed_files.append(file)
|
||||
|
||||
return None if not failed_files else failed_files
|
||||
|
||||
async def verify_game(self, pkg_version: str | Path | Path = None, ignore_mismatch=False):
|
||||
if pkg_version is None:
|
||||
pkg_version = self._gamedir.joinpath("pkg_version")
|
||||
return await self.verify_from_pkg_version(pkg_version, ignore_mismatch)
|
||||
|
||||
async def clear_cache(self):
|
||||
await asyncio.to_thread(shutil.rmtree, self.temp_path, ignore_errors=True)
|
|
@ -1,32 +1,17 @@
|
|||
import aiohttp
|
||||
import locale
|
||||
|
||||
from aiopath import AsyncPath
|
||||
import aiohttp
|
||||
|
||||
from worthless import constants
|
||||
from pathlib import Path
|
||||
from worthless.classes import launcher, installer
|
||||
|
||||
|
||||
async def _get(url, **kwargs) -> dict:
|
||||
# Workaround because miHoYo uses retcode for their API instead of HTTP status code
|
||||
async with aiohttp.ClientSession() as session:
|
||||
rsp = await session.get(url, **kwargs)
|
||||
rsp_json = await rsp.json()
|
||||
if rsp_json["retcode"] != 0:
|
||||
# TODO: Add more information to the error message
|
||||
raise aiohttp.ClientResponseError(code=rsp_json["retcode"],
|
||||
message=rsp_json["message"],
|
||||
history=rsp.history,
|
||||
request_info=rsp.request_info)
|
||||
return rsp_json
|
||||
from worthless.game.gameenums import Variant
|
||||
|
||||
|
||||
def _get_system_language() -> str:
|
||||
"""Gets system language compatible with server parameters.
|
||||
|
||||
Return:
|
||||
System language with format xx-xx.
|
||||
System language with format xx-xx (e.g. en-us).
|
||||
"""
|
||||
|
||||
try:
|
||||
|
@ -42,54 +27,81 @@ class Launcher:
|
|||
Contains functions to get information from server and client like the official launcher.
|
||||
"""
|
||||
|
||||
def __init__(self, gamedir: str | Path = Path.cwd(), language: str = None, overseas=True):
|
||||
"""Initialize the launcher API
|
||||
|
||||
Args:
|
||||
gamedir (Path): Path to the game directory.
|
||||
"""
|
||||
self._overseas = overseas
|
||||
if overseas:
|
||||
def __init__(
|
||||
self,
|
||||
language: str = None,
|
||||
variant: Variant = None,
|
||||
):
|
||||
"""Initialize the launcher API"""
|
||||
if not variant:
|
||||
variant = Variant.INTERNATIONAL
|
||||
if not language:
|
||||
language = _get_system_language()
|
||||
self._variant = variant
|
||||
match variant:
|
||||
case Variant.INTERNATIONAL:
|
||||
self._api = constants.LAUNCHER_API_URL_OS
|
||||
self._params = {
|
||||
"key": "gcStgarh",
|
||||
"launcher_id": "10",
|
||||
}
|
||||
self._lang = language.lower().replace("_", "-") if language else _get_system_language()
|
||||
else:
|
||||
self._lang = language.lower().replace("_", "-")
|
||||
case Variant.CHINESE:
|
||||
self._api = constants.LAUNCHER_API_URL_CN
|
||||
self._params = {
|
||||
"key": "eYd89JmJ",
|
||||
"launcher_id": "18",
|
||||
"channel_id": "1"
|
||||
"channel_id": "1",
|
||||
}
|
||||
self._lang = "zh-cn" # Use chinese language because this is chinese version
|
||||
if isinstance(gamedir, str | AsyncPath):
|
||||
gamedir = Path(gamedir)
|
||||
self._gamedir = gamedir.resolve()
|
||||
self._lang = (
|
||||
"zh-cn" # Uses Chinese language because this is Chinese version
|
||||
)
|
||||
case Variant.BILIBILI:
|
||||
self._api = constants.LAUNCHER_API_URL_CN
|
||||
self._params = {
|
||||
"key": "KAtdSsoQ",
|
||||
"launcher_id": "17",
|
||||
"channel_id": "14",
|
||||
}
|
||||
self._lang = (
|
||||
"zh-cn" # Use Chinese language because this is Chinese version
|
||||
)
|
||||
# There is one for beta version too, but I don't have access to that version
|
||||
self._session = aiohttp.ClientSession()
|
||||
|
||||
async def _get(self, url, **kwargs) -> dict:
|
||||
# Workaround because miHoYo uses retcode for their API instead of HTTP status code (always 200)
|
||||
rsp = await self._session.get(url, **kwargs)
|
||||
rsp_json = await rsp.json()
|
||||
if rsp_json["retcode"] != 0:
|
||||
# TODO: Add more information to the error message
|
||||
raise aiohttp.ClientResponseError(
|
||||
message=rsp_json["message"],
|
||||
history=rsp.history,
|
||||
request_info=rsp.request_info,
|
||||
headers=rsp.headers,
|
||||
status=rsp_json["retcode"],
|
||||
)
|
||||
return rsp_json
|
||||
|
||||
@property
|
||||
def lang(self):
|
||||
return self._lang
|
||||
|
||||
async def _get_launcher_info(self, adv=True) -> launcher.Info:
|
||||
params = self._params | {"filter_adv": str(adv).lower(),
|
||||
"language": self._lang}
|
||||
rsp = await _get(self._api + "/content", params=params)
|
||||
params = self._params | {"filter_adv": str(adv).lower(), "language": self._lang}
|
||||
rsp = await self._get(self._api + "/content", params=params)
|
||||
if rsp["data"]["adv"] is None:
|
||||
params["language"] = "en-us"
|
||||
rsp = await _get(self._api + "/content", params=params)
|
||||
rsp = await self._get(self._api + "/content", params=params)
|
||||
lc_info = launcher.Info.from_dict(rsp["data"])
|
||||
return lc_info
|
||||
|
||||
async def override_gamedir(self, gamedir: str | Path) -> None:
|
||||
"""Overrides game directory with another directory.
|
||||
@lang.setter
|
||||
def lang(self, language: str) -> None:
|
||||
"""Sets current language to another language in xx-xx format.
|
||||
|
||||
Args:
|
||||
gamedir (str): New directory to override with.
|
||||
"""
|
||||
if isinstance(gamedir, str):
|
||||
gamedir = Path(gamedir).resolve()
|
||||
self._gamedir = gamedir
|
||||
|
||||
async def override_language(self, language: str) -> None:
|
||||
"""Overrides system detected language with another language.
|
||||
This method will automatically convert language with xx_XX to xx-xx (e.g. en_US to en-us)
|
||||
|
||||
Args:
|
||||
language (str): Language to override with.
|
||||
|
@ -108,7 +120,7 @@ class Launcher:
|
|||
aiohttp.ClientResponseError: An error occurred while fetching the information.
|
||||
"""
|
||||
|
||||
rsp = await _get(self._api + "/resource", params=self._params)
|
||||
rsp = await self._get(self._api + "/resource", params=self._params)
|
||||
return installer.Resource.from_dict(rsp["data"])
|
||||
|
||||
async def get_launcher_info(self) -> launcher.Info:
|
||||
|
@ -134,15 +146,3 @@ class Launcher:
|
|||
"""
|
||||
|
||||
return await self._get_launcher_info(adv=False)
|
||||
|
||||
async def get_launcher_background_url(self) -> str:
|
||||
"""Gets launcher background image url from the server.
|
||||
|
||||
Returns:
|
||||
Background image url.
|
||||
Raises:
|
||||
aiohttp.ClientResponseError: An error occurred while fetching the background image.
|
||||
"""
|
||||
|
||||
rsp = await self.get_launcher_info()
|
||||
return rsp.background.background
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
from configparser import ConfigParser
|
||||
from pathlib import Path
|
||||
|
||||
from aiopath import AsyncPath
|
||||
|
||||
|
||||
class LauncherConfig:
|
||||
"""
|
||||
Provides config.ini for official launcher compatibility
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create_config(game_version, overseas=True):
|
||||
"""
|
||||
Creates config.ini
|
||||
"""
|
||||
sub_channel = "0" if overseas else "1"
|
||||
config = ConfigParser()
|
||||
config.add_section("General")
|
||||
config.set("General", "channel", "1")
|
||||
config.set("General", "cps", "mihoyo")
|
||||
config.set("General", "game_version", game_version)
|
||||
config.set("General", "sdk_version", "")
|
||||
config.set("General", "sub_channel", sub_channel)
|
||||
return config
|
||||
|
||||
def __init__(self, config_path, game_version=None, overseas=True):
|
||||
if isinstance(config_path, str | AsyncPath):
|
||||
config_path = Path(config_path)
|
||||
if not game_version:
|
||||
game_version = "0.0.0"
|
||||
self.config_path = config_path
|
||||
self.config = ConfigParser()
|
||||
if self.config_path.exists():
|
||||
self.config.read(self.config_path)
|
||||
else:
|
||||
self.config = self.create_config(game_version, overseas)
|
||||
|
||||
def set_game_version(self, game_version):
|
||||
self.config.set("General", "game_version", game_version)
|
||||
|
||||
def set_overseas(self, overseas=True):
|
||||
sub_channel = "0" if overseas else "1"
|
||||
self.config.set("General", "sub_channel", sub_channel)
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Saves config.ini
|
||||
"""
|
||||
with self.config_path.open("w") as config_file:
|
||||
self.config.write(config_file)
|
|
@ -1,19 +1,18 @@
|
|||
import asyncio
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from aiopath import AsyncPath
|
||||
|
||||
|
||||
class LinuxUtils:
|
||||
"""Utilities for Linux-specific tasks.
|
||||
"""
|
||||
"""Utilities for Linux-specific tasks."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
async def _exec_command(args):
|
||||
"""Execute a command using pkexec (friendly gui)
|
||||
"""
|
||||
if not await AsyncPath("/usr/bin/pkexec").exists():
|
||||
"""Execute a command using pkexec (friendly gui)"""
|
||||
if not Path("/usr/bin/pkexec").is_file():
|
||||
raise FileNotFoundError("pkexec not found.")
|
||||
rsp = await asyncio.create_subprocess_shell(args)
|
||||
await rsp.wait()
|
||||
|
@ -25,16 +24,16 @@ class LinuxUtils:
|
|||
|
||||
return rsp
|
||||
|
||||
async def write_text_to_file(self, text, file_path: str | Path | AsyncPath):
|
||||
"""Write text to a file using pkexec (friendly gui)
|
||||
"""
|
||||
if isinstance(file_path, Path | AsyncPath):
|
||||
async def write_text_to_file(self, text, file_path: PathLike):
|
||||
"""Write text to a file using pkexec (friendly gui)"""
|
||||
if isinstance(file_path, Path):
|
||||
file_path = str(file_path)
|
||||
await self._exec_command('echo -e "{}" | pkexec tee {}'.format(text, file_path))
|
||||
|
||||
async def append_text_to_file(self, text, file_path: str | Path | AsyncPath):
|
||||
"""Append text to a file using pkexec (friendly gui)
|
||||
"""
|
||||
async def append_text_to_file(self, text, file_path: PathLike):
|
||||
"""Append text to a file using pkexec (friendly gui)"""
|
||||
if isinstance(file_path, Path):
|
||||
file_path = str(file_path)
|
||||
await self._exec_command('echo -e "{}" | pkexec tee -a {}'.format(text, file_path))
|
||||
await self._exec_command(
|
||||
'echo -e "{}" | pkexec tee -a {}'.format(text, file_path)
|
||||
)
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import asyncio
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
from aiopath import AsyncPath
|
||||
import aiohttp
|
||||
|
||||
from worthless import constants
|
||||
from worthless.game import Game as Installer
|
||||
from worthless.launcher import Launcher
|
||||
from worthless.installer import Installer
|
||||
|
||||
match platform.system():
|
||||
case "Linux":
|
||||
|
@ -28,21 +27,29 @@ except ImportError:
|
|||
|
||||
|
||||
class Patcher:
|
||||
def __init__(self, gamedir: Path | AsyncPath | str = AsyncPath.cwd(), data_dir: str | Path | AsyncPath = None,
|
||||
patch_url: str = None, overseas=True, patch_provider="Krock"):
|
||||
def __init__(
|
||||
self,
|
||||
gamedir: Path | str = None,
|
||||
data_dir: str | Path = None,
|
||||
patch_url: str = None,
|
||||
overseas=True,
|
||||
patch_provider="Krock",
|
||||
):
|
||||
if isinstance(gamedir, str | Path):
|
||||
gamedir = AsyncPath(gamedir)
|
||||
gamedir = Path(gamedir)
|
||||
self._gamedir = gamedir
|
||||
if not patch_url:
|
||||
patch_url = constants.PATCH_LIST[patch_provider].replace("http://", "https://")
|
||||
self._patch_url = (patch_url if patch_url else constants.PATCH_GIT_URL).replace('http://', 'https://')
|
||||
patch_url = constants.PATCH_LIST[patch_provider].replace(
|
||||
"http://", "https://"
|
||||
)
|
||||
self._patch_url = patch_url
|
||||
if not data_dir:
|
||||
self._appdirs = constants.APPDIRS
|
||||
self._patch_path = AsyncPath(self._appdirs.user_data_dir).joinpath("Patch")
|
||||
self._temp_path = AsyncPath(self._appdirs.user_cache_dir).joinpath("Patcher")
|
||||
self._patch_path = Path(self._appdirs.user_data_dir).joinpath("Patch")
|
||||
self._temp_path = Path(self._appdirs.user_cache_dir).joinpath("Patcher")
|
||||
else:
|
||||
if isinstance(data_dir, str | Path):
|
||||
data_dir = AsyncPath(data_dir)
|
||||
data_dir = Path(data_dir)
|
||||
self._patch_path = data_dir.joinpath("Patch")
|
||||
self._temp_path = data_dir.joinpath("Temp/Patcher")
|
||||
self._overseas = overseas
|
||||
|
@ -67,14 +74,14 @@ class Patcher:
|
|||
:return: Archive file in bytes
|
||||
"""
|
||||
# Replace http with https
|
||||
if self._patch_url.startswith('https://notabug.org'):
|
||||
archive_url = self._patch_url + '/archive/master.{}'.format(archive_format)
|
||||
if self._patch_url.startswith("https://notabug.org"):
|
||||
archive_url = self._patch_url + "/archive/master.{}".format(archive_format)
|
||||
return await (await self._get(archive_url)).read()
|
||||
try:
|
||||
url_split = self._patch_url.split('//')
|
||||
url_split = self._patch_url.split("//")
|
||||
git_server = url_split[0]
|
||||
git_owner, git_repo = url_split[1].split('/')
|
||||
archive_url = git_server + '/api/v1/repos/{}/{}/archive/{}.{}'.format(
|
||||
git_owner, git_repo = url_split[1].split("/")
|
||||
archive_url = git_server + "/api/v1/repos/{}/{}/archive/{}.{}".format(
|
||||
git_owner, git_repo, branch, archive_format
|
||||
)
|
||||
archive = await self._get(archive_url)
|
||||
|
@ -85,10 +92,17 @@ class Patcher:
|
|||
|
||||
async def _download_repo(self, fallback=False):
|
||||
if shutil.which("git") and not fallback:
|
||||
if not await self._patch_path.is_dir() or not await self._patch_path.joinpath(".git").exists():
|
||||
proc = await asyncio.create_subprocess_exec("git", "clone", self._patch_url, str(self._patch_path))
|
||||
if (
|
||||
not await self._patch_path.is_dir()
|
||||
or not await self._patch_path.joinpath(".git").exists()
|
||||
):
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"git", "clone", self._patch_url, str(self._patch_path)
|
||||
)
|
||||
else:
|
||||
proc = await asyncio.create_subprocess_exec("git", "pull", cwd=str(self._patch_path))
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"git", "pull", cwd=str(self._patch_path)
|
||||
)
|
||||
await proc.wait()
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError("Cannot download patch repository through git.")
|
||||
|
@ -134,7 +148,11 @@ class Patcher:
|
|||
for url in telemetry_url:
|
||||
try:
|
||||
await session.get("https://" + url, timeout=15)
|
||||
except (aiohttp.ClientResponseError, aiohttp.ClientConnectorError, asyncio.exceptions.TimeoutError):
|
||||
except (
|
||||
aiohttp.ClientResponseError,
|
||||
aiohttp.ClientConnectorError,
|
||||
asyncio.exceptions.TimeoutError,
|
||||
):
|
||||
continue
|
||||
else:
|
||||
unblocked_list.append(url)
|
||||
|
@ -158,32 +176,41 @@ class Patcher:
|
|||
gamever = "".join((await self._installer.get_game_version()).split("."))
|
||||
unity_path = self._gamedir.joinpath("UnityPlayer.dll")
|
||||
await unity_path.rename(self._gamedir.joinpath("UnityPlayer.dll.bak"))
|
||||
proc = await asyncio.create_subprocess_exec("xdelta3", "-d", "-s",
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"xdelta3",
|
||||
"-d",
|
||||
"-s",
|
||||
str(self._gamedir.joinpath("UnityPlayer.dll.bak")),
|
||||
str(self._patch_path.joinpath(
|
||||
"{}/patch_files/{}".format(gamever, patch))),
|
||||
str(self._gamedir.joinpath("UnityPlayer.dll")), cwd=self._gamedir)
|
||||
str(self._patch_path.joinpath("{}/patch_files/{}".format(gamever, patch))),
|
||||
str(self._gamedir.joinpath("UnityPlayer.dll")),
|
||||
cwd=self._gamedir,
|
||||
)
|
||||
await proc.wait()
|
||||
|
||||
async def _patch_xlua_fallback(self, patch):
|
||||
gamever = "".join((await self._installer.get_game_version()).split("."))
|
||||
data_name = self._installer.get_game_data_name()
|
||||
xlua_path = self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))
|
||||
await xlua_path.rename(self._gamedir.joinpath("{}/Plugins/xlua.dll.bak".format(data_name)))
|
||||
proc = await asyncio.create_subprocess_exec("xdelta3", "-d", "-s",
|
||||
str(self._gamedir.joinpath(
|
||||
"{}/Plugins/xlua.dll.bak".format(data_name))),
|
||||
str(self._patch_path.joinpath(
|
||||
"{}/patch_files/{}".format(gamever, patch))),
|
||||
str(self._gamedir.joinpath(
|
||||
"{}/Plugins/xlua.dll".format(data_name))),
|
||||
cwd=self._gamedir)
|
||||
await xlua_path.rename(
|
||||
self._gamedir.joinpath("{}/Plugins/xlua.dll.bak".format(data_name))
|
||||
)
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"xdelta3",
|
||||
"-d",
|
||||
"-s",
|
||||
str(self._gamedir.joinpath("{}/Plugins/xlua.dll.bak".format(data_name))),
|
||||
str(self._patch_path.joinpath("{}/patch_files/{}".format(gamever, patch))),
|
||||
str(self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))),
|
||||
cwd=self._gamedir,
|
||||
)
|
||||
await proc.wait()
|
||||
|
||||
async def _patch_unityplayer(self, patch):
|
||||
gamever = "".join((await self._installer.get_game_version()).split("."))
|
||||
unity_path = self._gamedir.joinpath("UnityPlayer.dll")
|
||||
patch_bytes = self._patch_path.joinpath("{}/patch_files/{}".format(gamever, patch)).read_bytes()
|
||||
patch_bytes = self._patch_path.joinpath(
|
||||
"{}/patch_files/{}".format(gamever, patch)
|
||||
).read_bytes()
|
||||
patched_unity_bytes = xdelta3.decode(unity_path.read_bytes(), patch_bytes)
|
||||
unity_path.rename(self._gamedir.joinpath("UnityPlayer.dll.bak"))
|
||||
with Path(self._gamedir.joinpath("UnityPlayer.dll")).open("wb") as f:
|
||||
|
@ -193,10 +220,16 @@ class Patcher:
|
|||
gamever = "".join((await self._installer.get_game_version()).split("."))
|
||||
data_name = self._installer.get_game_data_name()
|
||||
xlua_path = self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))
|
||||
patch_bytes = self._patch_path.joinpath("{}/patch_files/{}".format(gamever, patch)).read_bytes()
|
||||
patch_bytes = self._patch_path.joinpath(
|
||||
"{}/patch_files/{}".format(gamever, patch)
|
||||
).read_bytes()
|
||||
patched_xlua_bytes = xdelta3.decode(xlua_path.read_bytes(), patch_bytes)
|
||||
xlua_path.rename(self._gamedir.joinpath("{}/Plugins/xlua.dll.bak".format(data_name)))
|
||||
with Path(self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))).open("wb") as f:
|
||||
xlua_path.rename(
|
||||
self._gamedir.joinpath("{}/Plugins/xlua.dll.bak".format(data_name))
|
||||
)
|
||||
with Path(self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))).open(
|
||||
"wb"
|
||||
) as f:
|
||||
f.write(patched_xlua_bytes)
|
||||
|
||||
async def apply_xlua_patch(self, fallback=True):
|
||||
|
@ -237,32 +270,25 @@ class Patcher:
|
|||
disable_files = [
|
||||
self._installer.get_game_data_name() + "upload_crash.exe",
|
||||
self._installer.get_game_data_name() + "Plugins/crashreport.exe",
|
||||
self._installer.get_game_data_name() + "blueReporter.exe",
|
||||
]
|
||||
for file in disable_files:
|
||||
file_path = Path(self._gamedir.joinpath(file)).resolve()
|
||||
if file_path.exists():
|
||||
await AsyncPath(file_path).rename(str(file_path) + ".bak")
|
||||
|
||||
# Delete old Telemetry.dll on Linux (cAsE sEnsItIvE)
|
||||
if platform.system() == "Linux":
|
||||
telemetry_path = Path(self._installer.get_game_data_name()).joinpath("Plugins/Telemetry.dll")
|
||||
if telemetry_path.exists() and Path(self._installer.get_game_data_name()).joinpath("Plugins/telemetry.dll").exists():
|
||||
await telemetry_path.unlink()
|
||||
await Path(file_path).rename(str(file_path) + ".bak")
|
||||
|
||||
patch_jobs.append(disable_crashreporters())
|
||||
await asyncio.gather(*patch_jobs)
|
||||
|
||||
@staticmethod
|
||||
async def _creation_date(file_path: str | Path | AsyncPath):
|
||||
async def _creation_date(file_path: str | Path | Path):
|
||||
"""
|
||||
Try to get the date that a file was created, falling back to when it was
|
||||
last modified if that isn't possible.
|
||||
See http://stackoverflow.com/a/39501288/1709587 for explanation.
|
||||
"""
|
||||
if isinstance(file_path, str | Path):
|
||||
file_path = AsyncPath(file_path)
|
||||
if platform.system() == 'Windows':
|
||||
file_path = Path(file_path)
|
||||
if platform.system() == "Windows":
|
||||
return os.path.getctime(file_path)
|
||||
else:
|
||||
stat = await file_path.stat()
|
||||
|
@ -273,13 +299,23 @@ class Patcher:
|
|||
# so we'll settle for when its content was last modified.
|
||||
return stat.st_mtime
|
||||
|
||||
async def _revert_file(self, original_file: str, base_file: AsyncPath, ignore_error=False):
|
||||
async def _revert_file(
|
||||
self, original_file: str, base_file: Path, ignore_error=False
|
||||
):
|
||||
original_path = Path(self._gamedir.joinpath(original_file + ".bak")).resolve()
|
||||
target_file = Path(self._gamedir.joinpath(original_file)).resolve()
|
||||
if original_path.exists():
|
||||
if abs(await self._creation_date(base_file) - await self._creation_date(original_path)) > 86400: # 24 hours
|
||||
if (
|
||||
abs(
|
||||
await self._creation_date(base_file)
|
||||
- await self._creation_date(original_path)
|
||||
)
|
||||
> 86400
|
||||
): # 24 hours
|
||||
if not ignore_error:
|
||||
raise RuntimeError("{} is not for this game version.".format(original_path.name))
|
||||
raise RuntimeError(
|
||||
"{} is not for this game version.".format(original_path.name)
|
||||
)
|
||||
original_path.unlink(missing_ok=True)
|
||||
else:
|
||||
target_file.unlink(missing_ok=True)
|
||||
|
@ -291,13 +327,14 @@ class Patcher:
|
|||
|
||||
:return: None
|
||||
"""
|
||||
game_exec = self._gamedir.joinpath((await self._launcher.get_resource_info()).game.latest.entry)
|
||||
game_exec = self._gamedir.joinpath(
|
||||
(await self._launcher.get_resource_info()).game.latest.entry
|
||||
)
|
||||
revert_files = [
|
||||
"UnityPlayer.dll",
|
||||
self._installer.get_game_data_name() + "upload_crash.exe",
|
||||
self._installer.get_game_data_name() + "Plugins/crashreport.exe",
|
||||
self._installer.get_game_data_name() + "Plugins/xlua.dll",
|
||||
self._installer.get_game_data_name() + "blueReporter.exe",
|
||||
]
|
||||
revert_job = []
|
||||
for file in revert_files:
|
||||
|
@ -311,9 +348,9 @@ class Patcher:
|
|||
all_files.extend(Path(self._gamedir).glob(ext))
|
||||
return all_files
|
||||
|
||||
files = get_files(('*.dxvk-cache', '*_d3d9.log', '*_d3d11.log', '*_dxgi.log'))
|
||||
files = get_files(("*.dxvk-cache", "*_d3d9.log", "*_d3d11.log", "*_dxgi.log"))
|
||||
for file in files:
|
||||
revert_job.append(AsyncPath(file).unlink(missing_ok=True))
|
||||
revert_job.append(Path(file).unlink(missing_ok=True))
|
||||
|
||||
await asyncio.gather(*revert_job)
|
||||
|
||||
|
|
Loading…
Reference in New Issue