Support downloading game & voicepacks and install, bump to 1.1.0

In CLI too, also optimized code & added test for voiceover functions.
This commit is contained in:
tretrauit 2022-02-18 11:11:55 +07:00
parent 07ba17b576
commit bb37e4554d
Signed by: tretrauit
GPG Key ID: 862760FF1903319E
5 changed files with 194 additions and 26 deletions

View File

@ -9,7 +9,7 @@ README = (HERE / "README.md").read_text()
setup( setup(
name='worthless', name='worthless',
version='1.0.0', version='1.1.0',
packages=['worthless'], packages=['worthless'],
url='https://git.froggi.es/tretrauit/worthless-launcher', url='https://git.froggi.es/tretrauit/worthless-launcher',
license='MIT License', license='MIT License',

View File

@ -6,7 +6,7 @@ game_launcher = worthless.Launcher(overseas=False)
game_installer = worthless.Installer(overseas=False) game_installer = worthless.Installer(overseas=False)
class LauncherOverseasTest(unittest.TestCase): class LauncherCNTest(unittest.TestCase):
def test_get_version_info(self): def test_get_version_info(self):
version_info = asyncio.run(game_launcher.get_resource_info()) version_info = asyncio.run(game_launcher.get_resource_info())
print("get_resource_info test.") print("get_resource_info test.")

View File

@ -35,13 +35,26 @@ class LauncherOverseasTest(unittest.TestCase):
self.assertIsInstance(bg_url, str) self.assertIsInstance(bg_url, str)
self.assertTrue(bg_url) 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")) game_diff = asyncio.run(game_installer.get_game_diff_archive("2.4.0"))
print("get_game_diff_archive test.") print("get_game_diff_archive test.")
print("get_game_diff_archive: ", game_diff) print("get_game_diff_archive: ", game_diff)
print("raw: ", game_diff.raw) print("raw: ", game_diff.raw)
self.assertIsInstance(game_diff, installer.Diff) 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__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -1,6 +1,8 @@
#!/usr/bin/python3 #!/usr/bin/python3
import argparse import argparse
import asyncio
import appdirs import appdirs
from pathlib import Path from pathlib import Path
from worthless.launcher import Launcher from worthless.launcher import Launcher
@ -17,8 +19,11 @@ class UI:
self._installer = Installer(gamedir, data_dir=tempdir) self._installer = Installer(gamedir, data_dir=tempdir)
self._patcher = Patcher(gamedir) self._patcher = Patcher(gamedir)
@staticmethod def _ask(self, question):
def _ask(question): if self._noconfirm:
# Fake dialog
print(question + " (y/n): y")
return True
answer = "" answer = ""
while answer.lower() not in ['y', 'n']: while answer.lower() not in ['y', 'n']:
if answer != "": if answer != "":
@ -76,16 +81,76 @@ class UI:
self._install_from_archive(filepath) self._install_from_archive(filepath)
print("Game installed successfully.") print("Game installed successfully.")
def install_game(self): def install_game(self, forced: bool = False):
# TODO res_info = asyncio.run(self._launcher.get_resource_info())
raise NotImplementedError("Install game is not implemented.") 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): def update_game(self):
print("Checking for current game version...") game_ver = self._installer.get_game_version()
# Call check_game_version() if not game_ver:
print("Updating game...") self.install_game()
# Call update_game(fromver) return
raise NotImplementedError("Update game is not implemented.") 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): def interactive_ui(self):
raise NotImplementedError() raise NotImplementedError()
@ -109,9 +174,9 @@ def main():
else update from archive)") else update from archive)")
parser.add_argument("-Sp", "--patch", action="store_true", parser.add_argument("-Sp", "--patch", action="store_true",
help="Patch the game (if not already patched, else do nothing)") 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)") 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)") help="Update the voiceover pack only (or install if not found)")
parser.add_argument("-Syu", "--update-all", action="store_true", parser.add_argument("-Syu", "--update-all", action="store_true",
help="Update the game and all installed voiceover packs (or install if not found)") help="Update the game and all installed voiceover packs (or install if not found)")
@ -125,7 +190,7 @@ def main():
args = parser.parse_args() 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 \ 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.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: if args.temporary_dir:
args.temporary_dir.mkdir(parents=True, exist_ok=True) args.temporary_dir.mkdir(parents=True, exist_ok=True)
@ -150,7 +215,10 @@ def main():
ui.install_game() ui.install_game()
if args.update: 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: if args.install_from_file:
ui.install_from_file(args.install_from_file) ui.install_from_file(args.install_from_file)

View File

@ -1,13 +1,14 @@
import re import re
import shutil import shutil
import aiohttp
import appdirs import appdirs
import zipfile import zipfile
import warnings import warnings
import json import json
from pathlib import Path from pathlib import Path
from configparser import ConfigParser from configparser import ConfigParser
from aiopath import AsyncPath
from worthless import constants from worthless import constants
from worthless.launcher import Launcher from worthless.launcher import Launcher
@ -75,11 +76,12 @@ class Installer:
self._gamedir = gamedir self._gamedir = gamedir
if not data_dir: if not data_dir:
self._appdirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR) 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: else:
if not isinstance(data_dir, Path): if not isinstance(data_dir, Path):
data_dir = Path(data_dir) 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") config_file = self._gamedir.joinpath("config.ini")
self._config_file = config_file.resolve() self._config_file = config_file.resolve()
self._version = None self._version = None
@ -87,6 +89,33 @@ class Installer:
self._launcher = Launcher(self._gamedir, overseas=self._overseas) self._launcher = Launcher(self._gamedir, overseas=self._overseas)
self._version = self.get_game_version() 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): def get_game_archive_version(self, game_archive: str | Path):
if not game_archive.exists(): if not game_archive.exists():
raise FileNotFoundError(f"Game archive {game_archive} not found") raise FileNotFoundError(f"Game archive {game_archive} not found")
@ -110,6 +139,7 @@ class Installer:
return "zh-cn" return "zh-cn"
case "Korean": case "Korean":
return "ko-kr" return "ko-kr"
return lang
@staticmethod @staticmethod
def get_voiceover_archive_language(voiceover_archive: str | Path): def get_voiceover_archive_language(voiceover_archive: str | Path):
@ -174,20 +204,53 @@ class Installer:
archive.extractall(self._gamedir, members=files) archive.extractall(self._gamedir, members=files)
archive.close() archive.close()
# Update game version on local variable.
self._version = self.get_game_version()
async def download_game_update(self): async def download_full_game(self):
if self._version is None: archive = await self._launcher.get_resource_info()
raise ValueError("Game version not found, use install_game to install the game.") 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() version_info = await self._launcher.get_resource_info()
if version_info is None: if version_info is None:
raise RuntimeError("Failed to fetch game resource info.") raise RuntimeError("Failed to fetch game resource info.")
if self._version == version_info.game.latest.version: if self._version == version_info.game.latest.version:
raise ValueError("Game is already up to date.") 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: if diff_archive is None:
raise ValueError("Game diff archive is not available for this version, please reinstall.") raise ValueError("Game diff archive is not available for this version, please reinstall.")
# TODO: Download the diff archive await self._download_file(diff_archive.path, diff_archive.name, diff_archive.size)
raise NotImplementedError("Downloading game diff archive is not implemented yet.")
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): def uninstall_game(self):
shutil.rmtree(self._gamedir) shutil.rmtree(self._gamedir)
@ -211,6 +274,30 @@ class Installer:
archive.extractall(self._gamedir) archive.extractall(self._gamedir)
archive.close() 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): async def get_game_diff_archive(self, from_version: str = None):
"""Gets a diff archive from `from_version` to the latest one """Gets a diff archive from `from_version` to the latest one