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
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)