From efc7ff2be94e2795d324891503811092a0b3242a Mon Sep 17 00:00:00 2001 From: tretrauit Date: Tue, 20 Jun 2023 18:14:44 +0700 Subject: [PATCH] fix: implement segments download michos don't fucking do breaking changes in the API fuck you. --- worthless/cli.py | 0 worthless/installer.py | 97 ++++++++++++++++++++++++------------------ 2 files changed, 55 insertions(+), 42 deletions(-) mode change 100644 => 100755 worthless/cli.py diff --git a/worthless/cli.py b/worthless/cli.py old mode 100644 new mode 100755 diff --git a/worthless/installer.py b/worthless/installer.py index 2c5b8e3..a3bd2fd 100644 --- a/worthless/installer.py +++ b/worthless/installer.py @@ -9,7 +9,6 @@ from configparser import ConfigParser from pathlib import Path import aiohttp -from aiopath import AsyncPath from worthless import constants from worthless.launcher import Launcher @@ -25,16 +24,16 @@ async def _download_file(file_url: str, file_name: str, file_path: Path | str, f :return: """ headers = {} - file_path = AsyncPath(file_path).joinpath(file_name) + file_path = Path(file_path).joinpath(file_name) if overwrite: await file_path.unlink(missing_ok=True) - if await file_path.exists(): - cur_len = (await file_path.stat()).st_size + if file_path.exists(): + cur_len = (file_path.stat()).st_size headers |= { "Range": f"bytes={cur_len}-{file_len if file_len else ''}" } else: - await file_path.touch() + 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) @@ -46,8 +45,8 @@ async def _download_file(file_url: str, file_name: str, file_path: Path | str, f await asyncio.sleep(0) if not chunk: break - async with file_path.open("ab") as f: - await f.write(chunk) + with file_path.open("ab") as f: + f.write(chunk) def calculate_md5(file_to_calculate): @@ -167,17 +166,17 @@ class HDiffPatch: class Installer: - def __init__(self, gamedir: str | Path | AsyncPath = AsyncPath.cwd(), - overseas: bool = True, data_dir: str | Path | AsyncPath = None): + def __init__(self, gamedir: str | Path = Path.cwd(), + overseas: bool = True, data_dir: str | Path = None): if isinstance(gamedir, str | Path): - gamedir = AsyncPath(gamedir) + gamedir = Path(gamedir) self._gamedir = gamedir if not data_dir: self._appdirs = constants.APPDIRS - self.temp_path = AsyncPath(self._appdirs.user_cache_dir).joinpath("Installer") + self.temp_path = Path(self._appdirs.user_cache_dir).joinpath("Installer") else: - if isinstance(data_dir, str | AsyncPath): - data_dir = AsyncPath(data_dir) + 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") @@ -201,13 +200,13 @@ class Installer: chunks=self._download_chunk) async def read_version_from_config(self): - if not await self._config_file.exists(): + 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: AsyncPath | Path | bytes) -> str: + async def read_version_from_game_file(self, globalgamemanagers: Path | Path | bytes) -> str: """ Reads the version from the globalgamemanagers file. (Data/globalgamemanagers) @@ -216,9 +215,9 @@ class Installer: :return: Game version (ex 1.0.0) """ - if isinstance(globalgamemanagers, Path | AsyncPath): - globalgamemanagers = AsyncPath(globalgamemanagers) - data = await globalgamemanagers.read_text("ascii", errors="ignore") + 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) @@ -258,7 +257,7 @@ class Installer: return lang @staticmethod - async def get_voiceover_archive_language(voiceover_archive: str | Path | AsyncPath) -> str: + 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(): @@ -293,7 +292,7 @@ class Installer: else: return "YuanShen_Data/" - def get_game_data_path(self) -> AsyncPath: + 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): @@ -307,7 +306,7 @@ class Installer: async def get_game_version(self) -> str | None: globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers") - if not await globalgamemanagers.exists(): + if not globalgamemanagers.exists(): try: return await self.read_version_from_config() except FileNotFoundError: @@ -323,11 +322,11 @@ class Installer: voiceovers = [] async for file in self.get_game_data_path()\ .joinpath("StreamingAssets/AudioAssets/").iterdir(): - if await file.is_dir(): + if file.is_dir(): voiceovers.append(file.name) return voiceovers - async def _update(self, game_archive: str | Path | AsyncPath): + async def _update(self, game_archive: str | Path | Path): archive = zipfile.ZipFile(game_archive, 'r') if not self._hdiffpatch.get_hpatchz_executable(): @@ -370,7 +369,7 @@ class Installer: count = 0 for file in hdifffiles: current_game_file = self._gamedir.joinpath(file) - if not await current_game_file.exists(): + if not current_game_file.exists(): print("File", file, "not found") # Not patching since we don't have the file continue @@ -387,18 +386,18 @@ class Installer: print("Failed to extract diff file", diff_file) return old_suffix = old_file.suffix - old_file = await old_file.rename(old_file.with_suffix(".bak")) + 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: - await old_file.unlink() + 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)) ) - await old_file.rename(old_file.with_suffix(old_suffix)) + old_file.rename(old_file.with_suffix(old_suffix)) files.remove(patch_file) # Limit to 8 process running so it doesn't hang the PC. @@ -422,8 +421,8 @@ class Installer: await asyncio.to_thread(archive.extractall, self._gamedir, members=files) archive.close() - async def update_game(self, game_archive: str | Path | AsyncPath): - if not await self.get_game_data_path().exists(): + 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() @@ -443,10 +442,24 @@ class Installer: async def download_full_game(self, pre_download=False): game = await self._get_game(pre_download) - archive_name = game.latest.path.split("/")[-1] - await self._download_file(game.latest.path, archive_name, game.latest.size) - if calculate_md5(self.temp_path.joinpath(archive_name)) != game.latest.md5: - raise RuntimeError("mismatch md5 for downloaded game archive") + 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 + 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() + async def download_full_voiceover(self, language: str, pre_download=False): game = await self._get_game(pre_download) @@ -458,8 +471,8 @@ class Installer: 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 | AsyncPath): - if isinstance(archive, str | AsyncPath): + 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") @@ -470,24 +483,24 @@ class Installer: # 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 await self.get_game_data_path().exists(): + 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 | AsyncPath, force_reinstall: bool = False): + 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 await self.get_game_data_path().exists(): + 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() - await self._gamedir.mkdir(parents=True, exist_ok=True) + 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() @@ -559,8 +572,8 @@ class Installer: if v.version == from_version: return v - async def verify_from_pkg_version(self, pkg_version: AsyncPath, ignore_mismatch=False): - contents = await pkg_version.read_text() + 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) @@ -596,7 +609,7 @@ class Installer: return None if not failed_files else failed_files - async def verify_game(self, pkg_version: str | Path | AsyncPath = None, ignore_mismatch=False): + 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)