fix: implement segments download

michos don't fucking do breaking changes in the API fuck you.
This commit is contained in:
tretrauit 2023-06-20 18:14:44 +07:00
parent 5e46b23752
commit efc7ff2be9
2 changed files with 55 additions and 42 deletions

0
worthless/cli.py Normal file → Executable file
View File

View File

@ -9,7 +9,6 @@ from configparser import ConfigParser
from pathlib import Path from pathlib import Path
import aiohttp import aiohttp
from aiopath import AsyncPath
from worthless import constants from worthless import constants
from worthless.launcher import Launcher 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: :return:
""" """
headers = {} headers = {}
file_path = AsyncPath(file_path).joinpath(file_name) file_path = Path(file_path).joinpath(file_name)
if overwrite: if overwrite:
await file_path.unlink(missing_ok=True) await file_path.unlink(missing_ok=True)
if await file_path.exists(): if file_path.exists():
cur_len = (await file_path.stat()).st_size cur_len = (file_path.stat()).st_size
headers |= { headers |= {
"Range": f"bytes={cur_len}-{file_len if file_len else ''}" "Range": f"bytes={cur_len}-{file_len if file_len else ''}"
} }
else: else:
await file_path.touch() file_path.touch()
print(f"Downloading {file_url} to {file_path}...") print(f"Downloading {file_url} to {file_path}...")
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=60*60, sock_read=240)) as session: 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) 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) await asyncio.sleep(0)
if not chunk: if not chunk:
break break
async with file_path.open("ab") as f: with file_path.open("ab") as f:
await f.write(chunk) f.write(chunk)
def calculate_md5(file_to_calculate): def calculate_md5(file_to_calculate):
@ -167,17 +166,17 @@ class HDiffPatch:
class Installer: class Installer:
def __init__(self, gamedir: str | Path | AsyncPath = AsyncPath.cwd(), def __init__(self, gamedir: str | Path = Path.cwd(),
overseas: bool = True, data_dir: str | Path | AsyncPath = None): overseas: bool = True, data_dir: str | Path = None):
if isinstance(gamedir, str | Path): if isinstance(gamedir, str | Path):
gamedir = AsyncPath(gamedir) gamedir = Path(gamedir)
self._gamedir = gamedir self._gamedir = gamedir
if not data_dir: if not data_dir:
self._appdirs = constants.APPDIRS 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: else:
if isinstance(data_dir, str | AsyncPath): if isinstance(data_dir, str | Path):
data_dir = AsyncPath(data_dir) data_dir = Path(data_dir)
self.temp_path = data_dir.joinpath("Temp/Installer/") self.temp_path = data_dir.joinpath("Temp/Installer/")
Path(self.temp_path).mkdir(parents=True, exist_ok=True) Path(self.temp_path).mkdir(parents=True, exist_ok=True)
config_file = self._gamedir.joinpath("config.ini") config_file = self._gamedir.joinpath("config.ini")
@ -201,13 +200,13 @@ class Installer:
chunks=self._download_chunk) chunks=self._download_chunk)
async def read_version_from_config(self): 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") raise FileNotFoundError(f"Config file {self._config_file} not found")
cfg = ConfigParser() cfg = ConfigParser()
await asyncio.to_thread(cfg.read, str(self._config_file)) await asyncio.to_thread(cfg.read, str(self._config_file))
return cfg.get("General", "game_version") 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) Reads the version from the globalgamemanagers file. (Data/globalgamemanagers)
@ -216,9 +215,9 @@ class Installer:
:return: Game version (ex 1.0.0) :return: Game version (ex 1.0.0)
""" """
if isinstance(globalgamemanagers, Path | AsyncPath): if isinstance(globalgamemanagers, Path | Path):
globalgamemanagers = AsyncPath(globalgamemanagers) globalgamemanagers = Path(globalgamemanagers)
data = await globalgamemanagers.read_text("ascii", errors="ignore") data = globalgamemanagers.read_text("ascii", errors="ignore")
else: else:
data = globalgamemanagers.decode("ascii", errors="ignore") data = globalgamemanagers.decode("ascii", errors="ignore")
result = self._game_version_re.search(data) result = self._game_version_re.search(data)
@ -258,7 +257,7 @@ class Installer:
return lang return lang
@staticmethod @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): if isinstance(voiceover_archive, str | Path):
voiceover_archive = Path(voiceover_archive).resolve() voiceover_archive = Path(voiceover_archive).resolve()
if not voiceover_archive.exists(): if not voiceover_archive.exists():
@ -293,7 +292,7 @@ class Installer:
else: else:
return "YuanShen_Data/" 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()) return self._gamedir.joinpath(self.get_game_data_name())
async def get_game_archive_version(self, game_archive: str | Path): 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: async def get_game_version(self) -> str | None:
globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers") globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers")
if not await globalgamemanagers.exists(): if not globalgamemanagers.exists():
try: try:
return await self.read_version_from_config() return await self.read_version_from_config()
except FileNotFoundError: except FileNotFoundError:
@ -323,11 +322,11 @@ class Installer:
voiceovers = [] voiceovers = []
async for file in self.get_game_data_path()\ async for file in self.get_game_data_path()\
.joinpath("StreamingAssets/AudioAssets/").iterdir(): .joinpath("StreamingAssets/AudioAssets/").iterdir():
if await file.is_dir(): if file.is_dir():
voiceovers.append(file.name) voiceovers.append(file.name)
return voiceovers 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') archive = zipfile.ZipFile(game_archive, 'r')
if not self._hdiffpatch.get_hpatchz_executable(): if not self._hdiffpatch.get_hpatchz_executable():
@ -370,7 +369,7 @@ class Installer:
count = 0 count = 0
for file in hdifffiles: for file in hdifffiles:
current_game_file = self._gamedir.joinpath(file) 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") print("File", file, "not found")
# Not patching since we don't have the file # Not patching since we don't have the file
continue continue
@ -387,18 +386,18 @@ class Installer:
print("Failed to extract diff file", diff_file) print("Failed to extract diff file", diff_file)
return return
old_suffix = old_file.suffix 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), proc = await self._hdiffpatch.patch_file(old_file, old_file.with_suffix(old_suffix),
patch_path, wait=True) patch_path, wait=True)
patch_path.unlink() patch_path.unlink()
if proc.returncode == 0: if proc.returncode == 0:
await old_file.unlink() old_file.unlink()
return return
# Let the game download the file. # Let the game download the file.
print("Failed to patch {}, reverting and let the in-game updater do the job...".format( print("Failed to patch {}, reverting and let the in-game updater do the job...".format(
old_file.with_suffix(old_suffix)) 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) files.remove(patch_file)
# Limit to 8 process running so it doesn't hang the PC. # 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) await asyncio.to_thread(archive.extractall, self._gamedir, members=files)
archive.close() archive.close()
async def update_game(self, game_archive: str | Path | AsyncPath): async def update_game(self, game_archive: str | Path | Path):
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}") raise FileNotFoundError(f"Game not found in {self._gamedir}")
if isinstance(game_archive, str | Path): if isinstance(game_archive, str | Path):
game_archive = Path(game_archive).resolve() game_archive = Path(game_archive).resolve()
@ -443,10 +442,24 @@ class Installer:
async def download_full_game(self, pre_download=False): async def download_full_game(self, pre_download=False):
game = await self._get_game(pre_download) game = await self._get_game(pre_download)
if not game.latest.path == "":
archive_name = game.latest.path.split("/")[-1] 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: if calculate_md5(self.temp_path.joinpath(archive_name)) != game.latest.md5:
raise RuntimeError("mismatch md5 for downloaded game archive") 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): async def download_full_voiceover(self, language: str, pre_download=False):
game = await self._get_game(pre_download) game = await self._get_game(pre_download)
@ -458,8 +471,8 @@ class Installer:
async def uninstall_game(self): async def uninstall_game(self):
await asyncio.to_thread(shutil.rmtree, self._gamedir, ignore_errors=True) await asyncio.to_thread(shutil.rmtree, self._gamedir, ignore_errors=True)
async def _extract_game_file(self, archive: str | Path | AsyncPath): async def _extract_game_file(self, archive: str | Path | Path):
if isinstance(archive, str | AsyncPath): if isinstance(archive, str | Path):
archive = Path(archive).resolve() archive = Path(archive).resolve()
if not archive.exists(): if not archive.exists():
raise FileNotFoundError(f"'{archive}' not found") raise FileNotFoundError(f"'{archive}' not found")
@ -470,24 +483,24 @@ class Installer:
# Since Voiceover packages are unclear about diff package or full package # 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 # we will try to extract the voiceover package and apply it to the game
# making this function universal for both cases # 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}") raise FileNotFoundError(f"Game not found in {self._gamedir}")
if isinstance(voiceover_archive, str | Path): if isinstance(voiceover_archive, str | Path):
voiceover_archive = Path(voiceover_archive).resolve() voiceover_archive = Path(voiceover_archive).resolve()
await self._update(voiceover_archive) await self._update(voiceover_archive)
# await self._extract_game_file(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 """Installs the game to the current directory
If `force_reinstall` is True, the game will be uninstalled then reinstalled. 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: if not force_reinstall:
raise ValueError(f"Game is already installed in {self._gamedir}") raise ValueError(f"Game is already installed in {self._gamedir}")
await self.uninstall_game() 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) await self._extract_game_file(game_archive)
self._version = await self.get_game_version() self._version = await self.get_game_version()
self.set_version_config() self.set_version_config()
@ -559,8 +572,8 @@ class Installer:
if v.version == from_version: if v.version == from_version:
return v return v
async def verify_from_pkg_version(self, pkg_version: AsyncPath, ignore_mismatch=False): async def verify_from_pkg_version(self, pkg_version: Path, ignore_mismatch=False):
contents = await pkg_version.read_text() contents = pkg_version.read_text()
async def verify_file(file_to_verify, md5): async def verify_file(file_to_verify, md5):
print("Verifying file:", file_to_verify) print("Verifying file:", file_to_verify)
@ -596,7 +609,7 @@ class Installer:
return None if not failed_files else failed_files 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: if pkg_version is None:
pkg_version = self._gamedir.joinpath("pkg_version") pkg_version = self._gamedir.joinpath("pkg_version")
return await self.verify_from_pkg_version(pkg_version, ignore_mismatch) return await self.verify_from_pkg_version(pkg_version, ignore_mismatch)