diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index 63ca05f..79780d5 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -3,20 +3,25 @@ import logging import os import requests import subprocess +from typing import Union +from warnings import deprecated from shlex import shlex from pathlib import Path from dzgui.init.prereqs import has_steam_client +from dzgui.api.mods import _hash from dzgui.const.constants import ( + APPID_DAYZ, APP_NAME, DEBIAN_STEAM_PATH, DEFAULT_STEAM_PATH, FLATPAK_STEAM_PATH, UBUNTU_STEAM_PATH, + REQUEST_TIMEOUT, VDF_PATH, ) -from dzgui.const.endpoints import STEAM_PUBLISHED_FILES +from dzgui.const.endpoints import SUB_ENDPOINT, STEAM_PUBLISHED_FILES, UNSUB_ENDPOINT from dzgui.strings import wizard from dzgui.util.bash import concat_bash_args @@ -53,8 +58,6 @@ def get_steam_paths() -> list[tuple[Path, str]]: def concat_mods(mods: list[str]) -> str: - from dzgui.util.symlink import _hash - hashes = [] for mod in mods: md5sum = _hash(mod) @@ -73,21 +76,16 @@ def get_local_signatures(version_file: Path) -> dict[str, int]: return hashes -def enqueue_mod(client: str, mod: str, appid: int) -> None: - client_args = concat_bash_args(client) - subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod]) - - def get_needs_update( version_file: Path, remote_hashes: list[tuple[str, str, int, int]] ) -> list[tuple[str, str, int, int]]: local_hashes = get_local_signatures(version_file) needs_update: list[tuple[str, str, int, int]] = [] - for title, _id, _hash, size in remote_hashes: + for title, _id, mod_hash, size in remote_hashes: if _id not in local_hashes: - needs_update.append((title, _id, _hash, size)) + needs_update.append((title, _id, mod_hash, size)) elif _hash != local_hashes[_id]: - needs_update.append((title, _id, _hash, size)) + needs_update.append((title, _id, mod_hash, size)) else: continue return needs_update @@ -232,6 +230,28 @@ def vdf2json(path: Path) -> str: jbuf += "\n" +def update_workshop(key: str, mod: int, endpoint: str) -> None: + payload: dict[str, Union[int, str]] = { + "publishedfileid": mod, + "appid": APPID_DAYZ, + "key": key, + "list_type": 1, + "notify_client": 1, + } + try: + res = requests.post(endpoint, params=payload, timeout=REQUEST_TIMEOUT) + res.raise_for_status() + except Exception as e: + logger.critical(e) + + +def subscribe(key: str, mod: int) -> None: + update_workshop(key, mod, SUB_ENDPOINT) + + +def unsubscribe(key: str, mod: int) -> None: + update_workshop(key, mod, UNSUB_ENDPOINT) + def gen_shortcut() -> None: # TODO: """ @@ -244,3 +264,8 @@ def gen_shortcut() -> None: # or get right-most 32 bits # STEAMID_64 & 0xFFFFFFFF pass + +@deprecated("Use subscribe()") +def enqueue_mod(client: str, mod: str, appid: int) -> None: + client_args = concat_bash_args(client) + subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod]) diff --git a/dzgui/const/constants.py b/dzgui/const/constants.py index d9689d2..cee5a63 100644 --- a/dzgui/const/constants.py +++ b/dzgui/const/constants.py @@ -4,6 +4,7 @@ UDP_PORT = 27016 VM_FILE = "/proc/sys/vm/max_map_count" MIN_COUNT = 1048576 +RATE_LIMIT_THRESHOLD = 3 REQUEST_TIMEOUT = 10 APPNAME_DAYZ = "DayZ" diff --git a/dzgui/const/endpoints.py b/dzgui/const/endpoints.py index 621f233..9d15b02 100644 --- a/dzgui/const/endpoints.py +++ b/dzgui/const/endpoints.py @@ -1,6 +1,11 @@ # internal -STEAM_PUBLISHED_FILES = "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/?format=json" -STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?" +STEAM_PUBLISHED_FILES = ( + "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1" +) +STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1" +SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1" +UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1" + BM_SERVERS = "https://api.battlemetrics.com/servers?" GITHUB = "https://github.com/aclist" GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest" diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index c0d092e..4bca50a 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -12,10 +12,10 @@ import dzgui.api.servers as Servers from dzgui.api.steam import ( connect, - enqueue_mod, get_remote_signatures, get_needs_update, load_to_menu, + subscribe, ) from dzgui.api.mods import ( @@ -30,6 +30,7 @@ from dzgui.const.constants import ( APPID_DAYZ_EXP, APPNAME_DAYZ, APPNAME_DAYZ_EXP_HUMAN, + RATE_LIMIT_THRESHOLD, ) from dzgui.const.enum import NotebookPage, Preferences from dzgui.init.proc import is_dayz_running, is_steam_running @@ -294,15 +295,15 @@ class ConnectionManager: self.controller.open_page(NotebookPage.SERVERS) def _update_mods(self, raise_window: bool, menu_only: bool = False) -> None: - # NOTE: fast enqueue all mods in auto mode prefs = self.controller.get_prefs() + config_man = self.controller.get_config_man() + key = config_man.lookup(Preferences.STEAM) for title, mod, stamp, size in self.missing_mods: if self.controller.is_cancel_pending(): return - enqueue_mod(self.client, mod, self.appid) - # NOTE: prevents rate limiting - time.sleep(3) + subscribe(key, mod) + time.sleep(RATE_LIMIT_THRESHOLD) if raise_window is True: logger.info("Bringing window to foreground") diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index 05e9996..4fa8868 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -1,17 +1,22 @@ import logging -import shutil +import time from pathlib import Path from typing import TYPE_CHECKING +from dzgui.api.steam import unsubscribe from dzgui.api.mods import ( get_delimited_mods, - get_local_mod_path, find_stale_mods, _hash, remove_stale_signatures, ) -from dzgui.const.constants import APP_NAME, APPID_DAYZ, APPID_DAYZ_EXP +from dzgui.const.constants import ( + APP_NAME, + APPID_DAYZ, + APPID_DAYZ_EXP, + RATE_LIMIT_THRESHOLD, +) from dzgui.const.enum import Preferences from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.model.model_factory import FastInsertListStore, ModelFactory @@ -77,7 +82,7 @@ class ModManager: total_mods = len(self.store) self.emitter.emit("mods_updated", msg, total_mods) - def delete_mods(self) -> None: + def unsub_mods(self) -> None: sel = self.treeview.get_selection() model, pathlist = sel.get_selected_rows() # NOTE: reverse when multiple selection @@ -88,7 +93,7 @@ class ModManager: continue mod, _iter = res mods.append((mod, _iter)) - self.delete_mods_on_system(mods) + self.unsub_all_mods(mods) def get_mod_from_tree_path( self, tree_path: Gtk.TreePath @@ -101,24 +106,28 @@ class ModManager: return mod, tree_iter @call_on_thread(dialogs.deleting_mods) - def delete_mods_on_system(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None: + def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None: for mod, _iter in mods: - self.delete_single_mod(mod) + self.unsub_atomic_mod(mod) iters = [_iter for mod, _iter in mods] - func = StoredFunc(self._on_mods_deleted, iters) + func = StoredFunc(self._on_mods_unsubbed, iters) self.thread_man.set_cleanup_func(func) - def delete_single_mod(self, mod: str) -> None: + def unsub_atomic_mod(self, mod: str) -> None: + config_man = self.controller.get_config_man() + key = config_man.lookup(Preferences.STEAM) + unsubscribe(key, mod) + steam_path = Path(self.path) - mods_path = get_local_mod_path(steam_path) app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ) - md5 = _hash(mod) - symlink = app_path / md5 - symlink.unlink() - shutil.rmtree(mods_path / mod) - + try: + md5 = _hash(mod) + symlink = app_path / md5 + symlink.unlink() + except Exception as e: + logger.warning(e) # NOTE: second pass to unlink DAYZ_EXP mods # TODO: test this with working APPID_DAYZ_EXP installation try: @@ -127,8 +136,9 @@ class ModManager: symlink.unlink() except PeFile.AppNotInstalledError: pass + time.sleep(RATE_LIMIT_THRESHOLD) - def _on_mods_deleted(self, iters: list[Gtk.TreeIter]) -> None: + def _on_mods_unsubbed(self, iters: list[Gtk.TreeIter]) -> None: if self.store is None: return for _iter in iters: