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:
parent
07ba17b576
commit
bb37e4554d
2
setup.py
2
setup.py
@ -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',
|
||||||
|
@ -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.")
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user