diff --git a/setup.py b/setup.py index af48dfb..48896d0 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ README = (HERE / "README.md").read_text() setup( name='worthless', - version='1.0.0', + version='1.1.0', packages=['worthless'], url='https://git.froggi.es/tretrauit/worthless-launcher', license='MIT License', diff --git a/tests/launcher_api_cn_test.py b/tests/launcher_api_cn_test.py index e54ad51..045c9a6 100644 --- a/tests/launcher_api_cn_test.py +++ b/tests/launcher_api_cn_test.py @@ -6,7 +6,7 @@ game_launcher = worthless.Launcher(overseas=False) game_installer = worthless.Installer(overseas=False) -class LauncherOverseasTest(unittest.TestCase): +class LauncherCNTest(unittest.TestCase): def test_get_version_info(self): version_info = asyncio.run(game_launcher.get_resource_info()) print("get_resource_info test.") diff --git a/tests/launcher_api_test.py b/tests/launcher_api_test.py index f361676..bb308cf 100644 --- a/tests/launcher_api_test.py +++ b/tests/launcher_api_test.py @@ -35,13 +35,26 @@ class LauncherOverseasTest(unittest.TestCase): self.assertIsInstance(bg_url, str) self.assertTrue(bg_url) - def test_get_installer_diff(self): + def test_get_installer_game_diff(self): game_diff = asyncio.run(game_installer.get_game_diff_archive("2.4.0")) print("get_game_diff_archive test.") print("get_game_diff_archive: ", game_diff) print("raw: ", game_diff.raw) self.assertIsInstance(game_diff, installer.Diff) + def test_get_installer_voiceover_diff_one(self): + game_diff = asyncio.run(game_installer.get_voiceover_diff_archive("en-us", "2.4.0")) + print("get_voiceover_diff_archive test one (en-us)") + print("get_voiceover_diff_archive: ", game_diff) + print("raw: ", game_diff.raw) + self.assertIsInstance(game_diff, installer.Voicepack) + + def test_get_installer_voiceover_diff_two(self): + game_diff = asyncio.run(game_installer.get_voiceover_diff_archive("en-us", "2.4.0")) + print("get_voiceover_diff_archive test two (English(US))") + print("get_voiceover_diff_archive: ", game_diff) + print("raw: ", game_diff.raw) + self.assertIsInstance(game_diff, installer.Voicepack) if __name__ == '__main__': unittest.main() diff --git a/worthless/gui.py b/worthless/gui.py index d1b4a0e..5856a9f 100755 --- a/worthless/gui.py +++ b/worthless/gui.py @@ -1,6 +1,8 @@ #!/usr/bin/python3 import argparse +import asyncio + import appdirs from pathlib import Path from worthless.launcher import Launcher @@ -17,8 +19,11 @@ class UI: self._installer = Installer(gamedir, data_dir=tempdir) self._patcher = Patcher(gamedir) - @staticmethod - def _ask(question): + def _ask(self, question): + if self._noconfirm: + # Fake dialog + print(question + " (y/n): y") + return True answer = "" while answer.lower() not in ['y', 'n']: if answer != "": @@ -76,16 +81,76 @@ class UI: self._install_from_archive(filepath) print("Game installed successfully.") - def install_game(self): - # TODO - raise NotImplementedError("Install game is not implemented.") + def install_game(self, forced: bool = False): + res_info = asyncio.run(self._launcher.get_resource_info()) + print("Latest game version: {}".format(res_info.game.latest.version)) + if not self._ask("Do you want to install the game?"): + print("Aborting game installation process.") + return + print("Downloading full game (This will take a long time)...") + asyncio.run(self._installer.download_full_game()) + print("Installing game...") + self._install_from_archive(self._installer.temp_path.joinpath(res_info.game.latest.name)) + + def install_voiceover(self, languages: str): + res_info = asyncio.run(self._launcher.get_resource_info()) + print("Latest game version: {}".format(res_info.game.latest.version)) + 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.") + return + print("Downloading voiceover pack (This will take a long time)...") + asyncio.run(self._installer.download_full_voiceover(lng)) + print("Installing voiceover pack...") + self._apply_voiceover_from_archive(self._installer.temp_path.joinpath(res_info.game.latest.name)) + break def update_game(self): - print("Checking for current game version...") - # Call check_game_version() - print("Updating game...") - # Call update_game(fromver) - raise NotImplementedError("Update game is not implemented.") + game_ver = self._installer.get_game_version() + if not game_ver: + self.install_game() + return + print("Current game installation detected. ({})".format(game_ver)) + diff_archive = asyncio.run(self._installer.get_game_diff_archive()) + res_info = asyncio.run(self._launcher.get_resource_info()) + if not diff_archive: + print("No game updates available.") + return + print("Latest game version: {}".format(res_info.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)...") + asyncio.run(self._installer.download_game_update()) + print("Installing game update...") + self.install_from_file(self._installer.temp_path.joinpath(res_info.game.latest.name)) + + def update_voiceover(self, languages: str): + game_ver = self._installer.get_game_version() + if not game_ver: + self.install_voiceover(languages) + return + print("Current game installation detected. ({})".format(game_ver)) + for lng in languages.split(" "): + diff_archive = asyncio.run(self._installer.get_voiceover_diff_archive(lng)) + # res_info = asyncio.run(self._launcher.get_resource_info()) + 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)...") + asyncio.run(self._installer.download_voiceover_update(lng)) + print("Installing voiceover update for {}...".format(lng)) + self._apply_voiceover_from_archive(self._installer.temp_path.joinpath(diff_archive.name)) + + def update_game_voiceover(self, languages: str): + self.update_game() + self.update_voiceover(languages) def interactive_ui(self): raise NotImplementedError() @@ -109,9 +174,9 @@ def main(): 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", + parser.add_argument("-Sy", "--update", action="store", type=str, help="Update the game and specified voiceover pack only (or install if not found)") - parser.add_argument("-Sv", "--update-voiceover", action="store_true", + 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)") @@ -125,7 +190,7 @@ def main(): args = parser.parse_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 and not args.get_game_version and not \ - args.install_voiceover_from_file + args.install_voiceover_from_file and not args.update_voiceover if args.temporary_dir: args.temporary_dir.mkdir(parents=True, exist_ok=True) @@ -150,7 +215,10 @@ def main(): ui.install_game() if args.update: - ui.update_game() + ui.update_game_voiceover(args.update) + + if args.update_voiceover: + ui.update_voiceover(args.update_voiceover) if args.install_from_file: ui.install_from_file(args.install_from_file) diff --git a/worthless/installer.py b/worthless/installer.py index 8e3f448..31d8927 100644 --- a/worthless/installer.py +++ b/worthless/installer.py @@ -1,13 +1,14 @@ import re import shutil +import aiohttp import appdirs import zipfile import warnings import json from pathlib import Path from configparser import ConfigParser - +from aiopath import AsyncPath from worthless import constants from worthless.launcher import Launcher @@ -75,11 +76,12 @@ class Installer: self._gamedir = gamedir if not data_dir: self._appdirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR) - self._temp_path = Path(self._appdirs.user_cache_dir).joinpath("Installer") + self.temp_path = Path(self._appdirs.user_cache_dir).joinpath("Installer") else: if not isinstance(data_dir, Path): data_dir = Path(data_dir) - self._temp_path = data_dir.joinpath("Temp/Installer/") + self.temp_path = data_dir.joinpath("Temp/Installer/") + self.temp_path.mkdir(parents=True, exist_ok=True) config_file = self._gamedir.joinpath("config.ini") self._config_file = config_file.resolve() self._version = None @@ -87,6 +89,33 @@ class Installer: self._launcher = Launcher(self._gamedir, overseas=self._overseas) self._version = self.get_game_version() + async def _download_file(self, file_url: str, file_name: str, file_len: int = None): + """ + Download file name to temporary directory, + :param file_url: + :param file_name: + :return: + """ + params = {} + file_path = AsyncPath(self.temp_path).joinpath(file_name) + if file_path.exists(): + async with file_path.open("rb") as f: + cur_len = len(await f.read()) + params |= { + "Range": f"bytes={cur_len}-{file_len if file_len else ''}" + } + else: + await file_path.touch() + async with aiohttp.ClientSession() as session: + rsp = await session.get(file_url, params=params, timeout=None) + rsp.raise_for_status() + while True: + chunk = await rsp.content.read(8192) + if not chunk: + break + async with file_path.open("ab") as f: + await f.write(chunk) + def get_game_archive_version(self, game_archive: str | Path): if not game_archive.exists(): raise FileNotFoundError(f"Game archive {game_archive} not found") @@ -110,6 +139,7 @@ class Installer: return "zh-cn" case "Korean": return "ko-kr" + return lang @staticmethod def get_voiceover_archive_language(voiceover_archive: str | Path): @@ -174,20 +204,53 @@ class Installer: archive.extractall(self._gamedir, members=files) archive.close() + # Update game version on local variable. + self._version = self.get_game_version() - async def download_game_update(self): - if self._version is None: - raise ValueError("Game version not found, use install_game to install the game.") + async def download_full_game(self): + archive = await self._launcher.get_resource_info() + if archive is None: + raise RuntimeError("Failed to fetch game resource info.") + if self._version == archive.game.latest.version: + raise ValueError("Game is already up to date.") + await self._download_file(archive.game.latest.path, archive.game.latest.name, archive.game.latest.size) + + async def download_full_voiceover(self, language: str): + archive = await self._launcher.get_resource_info() + if archive is None: + raise RuntimeError("Failed to fetch game resource info.") + translated_lang = self.voiceover_lang_translate(language) + for vo in archive.game.latest.voice_packs: + if vo.language == translated_lang: + await self._download_file(vo.path, vo.name, vo.size) + + async def download_game_update(self, from_version: str = None): + if not from_version: + self._version = from_version + if not from_version: + raise ValueError("Game version not found") version_info = await self._launcher.get_resource_info() if version_info is None: raise RuntimeError("Failed to fetch game resource info.") if self._version == version_info.game.latest.version: raise ValueError("Game is already up to date.") - diff_archive = self.get_game_diff_archive() + diff_archive = await self.get_game_diff_archive(from_version) if diff_archive is None: raise ValueError("Game diff archive is not available for this version, please reinstall.") - # TODO: Download the diff archive - raise NotImplementedError("Downloading game diff archive is not implemented yet.") + 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): + if not from_version: + self._version = from_version + if not from_version: + raise ValueError("Game version not found, use install_game to install the game.") + version_info = await self._launcher.get_resource_info() + if version_info is None: + raise RuntimeError("Failed to fetch game resource info.") + diff_archive = await self.get_voiceover_diff_archive(language, from_version) + 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) def uninstall_game(self): shutil.rmtree(self._gamedir) @@ -211,6 +274,30 @@ class Installer: archive.extractall(self._gamedir) archive.close() + async def get_voiceover_diff_archive(self, lang: str, from_version: str = None): + """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: + if self._version: + from_version = self._version + else: + from_version = self._version = self.get_game_version() + if not from_version: + raise ValueError("No game version found") + game_resource = await self._launcher.get_resource_info() + if not game_resource: + raise ValueError("Could not fetch game resource") + translated_lang = self.voiceover_lang_translate(lang) + for v in game_resource.game.diffs: + if v.version != from_version: + continue + for vo in v.voice_packs: + if vo.language != translated_lang: + continue + return vo + async def get_game_diff_archive(self, from_version: str = None): """Gets a diff archive from `from_version` to the latest one