Support chinese variant, QQ object in launcher and some code optimizations

This commit is contained in:
tretrauit 2022-02-16 00:49:33 +07:00
parent 048a7ac9d0
commit c22918673b
Signed by: tretrauit
GPG Key ID: 862760FF1903319E
19 changed files with 389 additions and 58 deletions

80
.gitignore vendored
View File

@ -152,3 +152,83 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
_trial_temp/
_trail_temp.lock

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) <year> <copyright holders> Copyright (c) 2022 tretrauit
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -1,3 +1,3 @@
# worthless-launcher # worthless-launcher
A worthless CLI launcher. A worthless CLI launcher written in Python.

View File

@ -1 +1,3 @@
aiohttp==3.8.1 aiohttp==3.8.1
appdirs~=1.4.4
aiofiles~=0.8.0

View File

@ -0,0 +1,38 @@
import unittest
import asyncio
import worthless
from worthless.classes import launcher
client = worthless.Launcher(overseas=False)
class LauncherCNTest(unittest.TestCase):
def test_get_version_info(self):
version_info = asyncio.run(client.get_version_info())
print("get_version_info test.")
print("get_version_info: ", version_info)
self.assertIsInstance(version_info, dict)
def test_get_launcher_info(self):
launcher_info = asyncio.run(client.get_launcher_info())
print("get_launcher_info test.")
print("get_launcher_info: ", launcher_info)
print("raw: ", launcher_info.raw)
self.assertIsInstance(launcher_info, launcher.Info)
def test_get_launcher_full_info(self):
launcher_info = asyncio.run(client.get_launcher_full_info())
print("get_launcher_full_info test.")
print("get_launcher_full_info: ", launcher_info)
print("raw: ", launcher_info.raw)
self.assertIsInstance(launcher_info, launcher.Info)
def test_get_launcher_background_url(self):
bg_url = asyncio.run(client.get_launcher_background_url())
print("get_launcher_background_url test.")
print("get_launcher_background_url: ", bg_url)
self.assertIsInstance(bg_url, str)
self.assertTrue(bg_url)
if __name__ == '__main__':
unittest.main()

View File

@ -5,7 +5,7 @@ from worthless.classes import launcher
client = worthless.Launcher() client = worthless.Launcher()
class LauncherTest(unittest.TestCase): class LauncherOverseasTest(unittest.TestCase):
def test_get_version_info(self): def test_get_version_info(self):
version_info = asyncio.run(client.get_version_info()) version_info = asyncio.run(client.get_version_info())
print("get_version_info test.") print("get_version_info test.")
@ -16,12 +16,14 @@ class LauncherTest(unittest.TestCase):
launcher_info = asyncio.run(client.get_launcher_info()) launcher_info = asyncio.run(client.get_launcher_info())
print("get_launcher_info test.") print("get_launcher_info test.")
print("get_launcher_info: ", launcher_info) print("get_launcher_info: ", launcher_info)
print("raw: ", launcher_info.raw)
self.assertIsInstance(launcher_info, launcher.Info) self.assertIsInstance(launcher_info, launcher.Info)
def test_get_launcher_full_info(self): def test_get_launcher_full_info(self):
launcher_info = asyncio.run(client.get_launcher_full_info()) launcher_info = asyncio.run(client.get_launcher_full_info())
print("get_launcher_full_info test.") print("get_launcher_full_info test.")
print("get_launcher_full_info: ", launcher_info) print("get_launcher_full_info: ", launcher_info)
print("raw: ", launcher_info.raw)
self.assertIsInstance(launcher_info, launcher.Info) self.assertIsInstance(launcher_info, launcher.Info)
def test_get_launcher_background_url(self): def test_get_launcher_background_url(self):

View File

@ -1,6 +1,6 @@
#!/usr/bin/python3 #!/usr/bin/python3
from worthless import gui import gui as gui
if __name__ == '__main__': if __name__ == '__main__':
gui.main() gui.main()

View File

@ -3,7 +3,7 @@ class Background:
Note that the `background` variable is an url to the background image, Note that the `background` variable is an url to the background image,
while the `url` variable contains an empty string, so it seems that the while the `url` variable contains an empty string, so it seems that the
`url` and `icon` variables are used by the official launcher itself. `url` and `icon` variables are not used by the official launcher itself.
Also, the launcher background checksum is using an algorithm which I Also, the launcher background checksum is using an algorithm which I
haven't found out yet, so you better not rely on it but instead rely haven't found out yet, so you better not rely on it but instead rely
@ -16,8 +16,9 @@ class Background:
- :class:`str` url: The url variable. - :class:`str` url: The url variable.
- :class:`str` version: The launcher background version. - :class:`str` version: The launcher background version.
- :class:`str` bg_checksum: The launcher background checksum. - :class:`str` bg_checksum: The launcher background checksum.
- :class:`dict` raw: The launcher background raw information in dict. - :class:`dict` raw: The launcher background raw information.
""" """
def __init__(self, background, icon, url, version, bg_checksum, raw): def __init__(self, background, icon, url, version, bg_checksum, raw):
"""Inits the launcher background class""" """Inits the launcher background class"""
self.background = background self.background = background
@ -31,4 +32,4 @@ class Background:
def from_dict(data) -> 'Background': def from_dict(data) -> 'Background':
"""Creates a launcher background from a dictionary.""" """Creates a launcher background from a dictionary."""
return Background(data["background"], data["icon"], data["url"], return Background(data["background"], data["icon"], data["url"],
data["version"], data["bg_checksum"], data) data["version"], data["bg_checksum"], data)

View File

@ -16,9 +16,10 @@ class IconButton:
- :class:`str` qr_img: The QR code url. - :class:`str` qr_img: The QR code url.
- :class:`str` qr_desc: The QR code description. - :class:`str` qr_desc: The QR code description.
- :class:`str` img_hover: The icon url when hovered over. - :class:`str` img_hover: The icon url when hovered over.
- :class:`dict[LauncherIconOtherLink]` other_links: Other links in the button. - :class:`list[LauncherIconOtherLink]` other_links: Other links in the button.
- :class:`dict` raw: The launcher background raw information in dict. - :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""" """Inits the launcher icon class"""
self.icon_id = icon_id self.icon_id = icon_id
@ -38,5 +39,4 @@ class IconButton:
for link in data['other_links']: for link in data['other_links']:
other_links.append(IconOtherLink.from_dict(link)) other_links.append(IconOtherLink.from_dict(link))
return IconButton(data["icon_id"], data["img"], data["tittle"], data["url"], data["qr_img"], return IconButton(data["icon_id"], data["img"], data["tittle"], data["url"], data["qr_img"],
data["qr_desc"], data["img_hover"], other_links, data) data["qr_desc"], data["img_hover"], other_links, data)

View File

@ -1,16 +1,33 @@
from worthless.classes.launcher import background, banner, iconbutton, post from worthless.classes.launcher import background, banner, iconbutton, post, qq
Background = background.Background Background = background.Background
Banner = banner.Banner Banner = banner.Banner
IconButton = iconbutton.IconButton IconButton = iconbutton.IconButton
Post = post.Post Post = post.Post
QQ = qq.QQ
class Info: class Info:
def __init__(self, lc_background: Background, lc_banner: list[Banner], icon: list[IconButton], lc_post: list[Post]): """Contains the launcher information
Note that QQ is not wrapped due to not having access to the chinese yuanshen launcher.
You can contribute to the project if you want :D
Attributes:
- :class:`worthless.classes.launcher.background.Background` background: The launcher background information.
- :class:`worthless.classes.launcher.banner.Banner` banner: The launcher banner information.
- :class:`worthless.classes.launcher.iconbutton.IconButton` icon: The launcher icon buttons information.
- :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):
self.background = lc_background self.background = lc_background
self.banner = lc_banner self.banner = lc_banner
self.icon = icon self.icon = icon
self.post = lc_post self.post = lc_post
self.qq = lc_qq
self.raw = raw
@staticmethod @staticmethod
def from_dict(data): def from_dict(data):
@ -24,5 +41,8 @@ class Info:
lc_post = [] lc_post = []
for p in data["post"]: for p in data["post"]:
lc_post.append(Post.from_dict(p)) lc_post.append(Post.from_dict(p))
return Info(bg, lc_banner, lc_icon, lc_post) lc_qq = []
for q in data["qq"]:
lc_qq.append(QQ.from_dict(q))
return Info(bg, lc_banner, lc_icon, lc_post, lc_qq, data)

View File

@ -13,13 +13,13 @@ class Post:
Attributes: Attributes:
- :class:`str` post_id: The launcher post id. - :class:`str` post_id: The launcher post id.
- :class:`str` type: The post type, as explained above. - :class:`str` type: The post type, can be POST_TYPE_ANNOUNCE, POST_TYPE_ACTIVITY and POST_TYPE_INFO
- :class:`str` tittle: The post title. - :class:`str` tittle: The post title.
- :class:`str` url: The post target url. - :class:`str` url: The post target url.
- :class:`str` show_time: The time when the post will be shown. - :class:`str` show_time: The time when the post will be shown.
- :class:`str` order: The post order. - :class:`str` order: The post order.
- :class:`str` title: The post title. - :class:`str` title: The post title.
- :class:`dict` raw: The banner raw information. - :class:`dict` raw: The post raw information.
""" """
def __init__(self, post_id, post_type, tittle, url, show_time, order, title, raw): def __init__(self, post_id, post_type, tittle, url, show_time, order, title, raw):
self.post_id = post_id self.post_id = post_id

View File

@ -0,0 +1,35 @@
class QQ:
"""Contains the launcher QQ information
Note that QQ is not wrapped due to not having access to the chinese yuanshen launcher.
You can contribute to the project if you want :D
Attributes:
- :class:`str` qq_id: The id of the QQ post
- :class:`str` name: The name of the QQ post
- :class:`int` number: The number of the QQ post
- :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
self.number = number
self.code = code
self.raw = raw
@staticmethod
def from_dict(raw: dict) -> 'QQ':
"""Creates a QQ object from a dictionary
Args:
raw (dict): The raw dictionary
Returns:
QQ: The QQ object
"""
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)

View File

@ -0,0 +1,8 @@
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

View File

@ -1,6 +1,11 @@
LAUNCHER_API_URL = "https://sdk-os-static.mihoyo.com/hk4e_global/mdk/launcher/api" APP_NAME="worthless"
APP_AUTHOR="tretrauit"
LAUNCHER_API_URL_OS = "https://sdk-os-static.mihoyo.com/hk4e_global/mdk/launcher/api"
LAUNCHER_API_URL_CN = "https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api"
PATCH_GIT_URL = "https://notabug.org/Krock/dawn" PATCH_GIT_URL = "https://notabug.org/Krock/dawn"
TELEMETRY_URL_LIST = [ TELEMETRY_URL_LIST = [
"log-upload-os.mihoyo.com", "log-upload-os.mihoyo.com",
"log-upload.mihoyo.com",
"overseauspider.yuanshen.com" "overseauspider.yuanshen.com"
"uspider.yuanshen.com"
] ]

View File

@ -4,40 +4,66 @@ import argparse
from pathlib import Path from pathlib import Path
def interactive_ui(gamedir=Path.cwd()): class UI:
raise NotImplementedError("Interactive UI is not implemented") def __init__(self, gamedir: str, noconfirm: bool) -> None:
self._noconfirm = noconfirm
self._gamedir = gamedir
def _ask(self, title, description):
raise NotImplementedError()
def update_game(gamedir=Path.cwd(), noconfirm=False): def install_game(self):
print("Checking for current game version...") # TODO
# Call check_game_version() raise NotImplementedError("Install game is not implemented.")
print("Updating game...")
# Call update_game(fromver) def update_game(self):
raise NotImplementedError("Update game is not implemented") print("Checking for current game version...")
# Call check_game_version()
print("Updating game...")
# Call update_game(fromver)
raise NotImplementedError("Update game is not implemented.")
def interactive_ui(self):
raise NotImplementedError()
def main(): def main():
parser = argparse.ArgumentParser(prog="worthless-launcher", description="A worthless launcher written in Python.") parser = argparse.ArgumentParser(prog="worthless", description="A worthless launcher written in Python.")
parser.add_argument("-D", "-d", "--dir", action="store", type=Path, default=Path.cwd(), parser.add_argument("-D", "-d", "--dir", action="store", type=Path, default=Path.cwd(),
help="Specify the game directory (default current working directory)") help="Specify the game directory (default current working directory)")
parser.add_argument("-I", "-i", "--install", action="store_true", parser.add_argument("-S", "--install", action="store_true",
help="Install the game (if not already installed, else do nothing)") help="Install the game (if not already installed, else do nothing)")
parser.add_argument("-U", "-u", "--update", action="store_true", help="Update the game (if not updated)") parser.add_argument("-U", "--install-from-file", action="store_true",
help="Install the game from the game 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("-Sy", "--update", action="store_true",
help="Update the game and specified voiceover pack only (or install if not found)")
parser.add_argument("-Syu", "--update", action="store_true",
help="Update the game and all installed voiceover packs (or install if not found)")
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("--noconfirm", action="store_true", parser.add_argument("--noconfirm", action="store_true",
help="Do not ask any questions. (Ignored in interactive mode)") help="Do not ask any for confirmation. (Ignored in interactive mode)")
args = parser.parse_args() args = parser.parse_args()
print(args) interactive_mode = not args.install and not args.install_from_file and not args.patch and not args.update and not \
args.remove and not args.remove_patch and not args.remove_voiceover
ui = UI(args.dir, args.noconfirm)
if args.install and args.update: if args.install and args.update:
raise ValueError("Cannot specify both --install and --update arguments.") raise ValueError("Cannot specify both --install and --update arguments.")
if args.install: if args.install:
raise NotImplementedError("Install game is not implemented") ui.install_game()
if args.update: if args.update:
update_game(args.dir, args.noconfirm) ui.update_game()
return return
interactive_ui(args.dir) if interactive_mode:
ui.interactive_ui()
if __name__ == "__main__": if __name__ == "__main__":

33
worthless/installer.py Normal file
View File

@ -0,0 +1,33 @@
import asyncio
import tarfile
import constants
import appdirs
import aiofiles
from pathlib import Path
import shutil
import aiohttp
from worthless.launcher import Launcher
from configparser import ConfigParser
class Installer:
def __init__(self, gamedir: str | Path = Path.cwd(), overseas: bool = True):
if isinstance(gamedir, str):
gamedir = Path(gamedir)
self._gamedir = gamedir
config_file = self._gamedir.joinpath("config.ini")
self._config_file = config_file.resolve()
self._version = None
self._overseas = overseas
self._launcher = Launcher(self._gamedir, self._overseas)
if config_file.exists():
self._version = self._read_version_from_config()
else: # TODO: Use An Anime Game Launcher method (which is more brutal, but it works)
self._version = "mangosus"
def _read_version_from_config(self):
if self._config_file.exists():
raise FileNotFoundError(f"Config file {self._config_file} not found")
cfg = ConfigParser()
cfg.read(str(self._config_file))
return cfg.get("miHoYo", "game_version")

View File

@ -10,14 +10,28 @@ class Launcher:
Contains functions to get information from server and client like the official launcher. Contains functions to get information from server and client like the official launcher.
""" """
def __init__(self, gamedir=Path.cwd()): def __init__(self, gamedir=Path.cwd(), language=None, overseas=True):
"""Initialize the launcher API """Initialize the launcher API
Args: Args:
gamedir (Path): Path to the game directory. gamedir (Path): Path to the game directory.
""" """
self._api = constants.LAUNCHER_API_URL self._overseas = overseas
self._lang = self._get_system_language() if overseas:
self._api = constants.LAUNCHER_API_URL_OS
self._params = {
"key": "gcStgarh",
"launcher_id": "10",
}
self._lang = self._get_system_language() if not language else language.lower().replace("_", "-")
else:
self._api = constants.LAUNCHER_API_URL_CN
self._params = {
"key": "eYd89JmJ",
"launcher_id": "18",
"channel_id": "1"
}
self._lang = "zh-cn" # Use chinese language because this is Pooh version
if isinstance(gamedir, str): if isinstance(gamedir, str):
gamedir = Path(gamedir) gamedir = Path(gamedir)
self._gamedir = gamedir.resolve() self._gamedir = gamedir.resolve()
@ -36,18 +50,6 @@ class Launcher:
request_info=rsp.request_info) request_info=rsp.request_info)
return rsp_json return rsp_json
async def _get_launcher_info(self, adv=True) -> launcher.Info:
params = {"key": "gcStgarh",
"filter_adv": str(adv).lower(),
"launcher_id": "10",
"language": self._lang}
rsp = await self._get(self._api + "/content", params=params)
if rsp["data"]["adv"] is None:
params["language"] = "en-us"
rsp = await self._get(self._api + "/content", params=params)
lc_info = launcher.Info.from_dict(rsp["data"])
return lc_info
@staticmethod @staticmethod
def _get_system_language() -> str: def _get_system_language() -> str:
"""Gets system language compatible with server parameters. """Gets system language compatible with server parameters.
@ -61,16 +63,27 @@ class Launcher:
lowercase_lang = lang.lower().replace("_", "-") lowercase_lang = lang.lower().replace("_", "-")
return lowercase_lang return lowercase_lang
except ValueError: except ValueError:
return "en-us" return "en-us" # Fallback to English if locale is not supported
async def override_gamedir(self, gamedir: str) -> None: async def _get_launcher_info(self, adv=True) -> launcher.Info:
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 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. """Overrides game directory with another directory.
Args: Args:
gamedir (str): New directory to override with. gamedir (str): New directory to override with.
""" """
if isinstance(gamedir, str):
self._gamedir = Path(gamedir).resolve() gamedir = Path(gamedir).resolve()
self._gamedir = gamedir
async def override_language(self, language: str) -> None: async def override_language(self, language: str) -> None:
"""Overrides system detected language with another language. """Overrides system detected language with another language.
@ -92,8 +105,7 @@ class Launcher:
aiohttp.ClientResponseError: An error occurred while fetching the information. aiohttp.ClientResponseError: An error occurred while fetching the information.
""" """
rsp = await self._get(self._api + "/resource", params={"key": "gcStgarh", rsp = await self._get(self._api + "/resource", params=self._params)
"launcher_id": "10"})
return rsp return rsp
async def get_launcher_info(self) -> launcher.Info: async def get_launcher_info(self) -> launcher.Info:

View File

@ -1,31 +1,96 @@
import asyncio
import tarfile
import constants import constants
import appdirs
import aiofiles
from pathlib import Path from pathlib import Path
import shutil
import aiohttp
class Patcher: class Patcher:
def __init__(self, gamedir=Path.cwd()): def __init__(self, gamedir=Path.cwd(), data_dir: str | Path = None, patch_url: str = None):
self._gamedir = gamedir self._gamedir = gamedir
self._patch_url = constants.PATCH_GIT_URL self._patch_url = (patch_url if patch_url else constants.PATCH_GIT_URL).replace('http://', 'https://')
if not data_dir:
self._appdirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR)
self._patch_path = Path(self._appdirs.user_data_dir).joinpath("Patch")
self._temp_path = Path(self._appdirs.user_cache_dir)
else:
if not isinstance(data_dir, Path):
override_data_dir = Path(data_dir)
self._patch_path = data_dir.joinpath("Patch")
self._temp_path = data_dir.joinpath("Temp")
@staticmethod
async def _get(url, **kwargs) -> aiohttp.ClientResponse:
async with aiohttp.ClientSession() as session:
rsp = await session.get(url, **kwargs)
rsp.raise_for_status()
return rsp
async def _get_git_archive(self, archive_format="tar.gz", branch="master"):
"""
Get the git archive of the patch repository.
This supports Gitea API and also introduce workaround for https://notabug.org
: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)
return await (await self._get(archive_url)).read()
try:
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, branch, archive_format
)
archive = await self._get(archive_url)
except aiohttp.ClientResponseError:
pass
else:
return await archive.read()
return
async def _download_repo(self):
if shutil.which("git"):
if not self._patch_path.exists() or not self._patch_path.is_dir() \
or not self._patch_path.joinpath(".git").exists():
await asyncio.create_subprocess_exec("git", "clone", self._patch_url, str(self._patch_path))
else:
await asyncio.create_subprocess_exec("git", "pull", cwd=str(self._patch_path))
else:
archive = await self._get_git_archive()
if not archive:
raise RuntimeError("Cannot download patch repository")
with tarfile.open(archive) as tar:
tar.extractall(self._patch_path)
def override_patch_url(self, url) -> None: def override_patch_url(self, url) -> None:
""" """
Override the patch url. Override the patch url.
:param url: Patch repository url, the url must be a valid git repository. :param url: Patch repository url, the url must be a valid git repository.
:return: None :return: None
""" """
self._patch_url = url self._patch_url = url
def download_patch(self) -> None: async def download_patch(self) -> None:
""" """
If `git` exists, this will clone the patch git url and save it to a temporary directory. If `git` exists, this will clone the patch git url and save it to a temporary directory.
Else, this will download the patch from the patch url and save it to a temporary directory. (Not reliable) Else, this will download the patch from the patch url and save it to a temporary directory. (Not reliable)
:return: None :return: None
""" """
pass await self._download_repo()
def apply_patch(self, crash_fix=False) -> None: def apply_patch(self, crash_fix=False) -> None:
""" """
Patch the game (and optionally patch the login door crash fix if specified) Patch the game (and optionally patch the login door crash fix if specified)
:param crash_fix: Whether to patch the login door crash fix or not :param crash_fix: Whether to patch the login door crash fix or not
:return: None :return: None
""" """
@ -34,6 +99,7 @@ class Patcher:
def revert_patch(self): def revert_patch(self):
""" """
Revert the patch (and revert the login door crash fix if patched) Revert the patch (and revert the login door crash fix if patched)
:return: None :return: None
""" """
pass pass