fix: implement segments download
michos don't fucking do breaking changes in the API fuck you.
This commit is contained in:
parent
5e46b23752
commit
efc7ff2be9
0
worthless/cli.py
Normal file → Executable file
0
worthless/cli.py
Normal file → Executable 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)
|
||||
|
Loading…
Reference in New Issue
Block a user