feat: unsubscribe from mods gracefully

This commit is contained in:
aclist committed 2026-06-17 02:56:26 +09:00
1 parent 179f92234a
commit 8d77d46eda
5 files changed
+76 -34

No files matched your search

+36 -11
View File
@@ -3,20 +3,25 @@ import logging
import os import os
import requests import requests
import subprocess import subprocess
from typing import Union
from warnings import deprecated
from shlex import shlex from shlex import shlex
from pathlib import Path from pathlib import Path
from dzgui.init.prereqs import has_steam_client from dzgui.init.prereqs import has_steam_client
from dzgui.api.mods import _hash
from dzgui.const.constants import ( from dzgui.const.constants import (
APPID_DAYZ,
APP_NAME, APP_NAME,
DEBIAN_STEAM_PATH, DEBIAN_STEAM_PATH,
DEFAULT_STEAM_PATH, DEFAULT_STEAM_PATH,
FLATPAK_STEAM_PATH, FLATPAK_STEAM_PATH,
UBUNTU_STEAM_PATH, UBUNTU_STEAM_PATH,
REQUEST_TIMEOUT,
VDF_PATH, 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.strings import wizard
from dzgui.util.bash import concat_bash_args 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: def concat_mods(mods: list[str]) -> str:
from dzgui.util.symlink import _hash
hashes = [] hashes = []
for mod in mods: for mod in mods:
md5sum = _hash(mod) md5sum = _hash(mod)
@@ -73,21 +76,16 @@ def get_local_signatures(version_file: Path) -> dict[str, int]:
return hashes 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( def get_needs_update(
version_file: Path, remote_hashes: list[tuple[str, str, int, int]] version_file: Path, remote_hashes: list[tuple[str, str, int, int]]
) -> list[tuple[str, str, int, int]]: ) -> list[tuple[str, str, int, int]]:
local_hashes = get_local_signatures(version_file) local_hashes = get_local_signatures(version_file)
needs_update: list[tuple[str, str, int, int]] = [] 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: 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]: elif _hash != local_hashes[_id]:
needs_update.append((title, _id, _hash, size)) needs_update.append((title, _id, mod_hash, size))
else: else:
continue continue
return needs_update return needs_update
@@ -232,6 +230,28 @@ def vdf2json(path: Path) -> str:
jbuf += "\n" 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: def gen_shortcut() -> None:
# TODO: # TODO:
""" """
@@ -244,3 +264,8 @@ def gen_shortcut() -> None:
# or get right-most 32 bits # or get right-most 32 bits
# STEAMID_64 & 0xFFFFFFFF # STEAMID_64 & 0xFFFFFFFF
pass 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])
+1
View File
@@ -4,6 +4,7 @@ UDP_PORT = 27016
VM_FILE = "/proc/sys/vm/max_map_count" VM_FILE = "/proc/sys/vm/max_map_count"
MIN_COUNT = 1048576 MIN_COUNT = 1048576
RATE_LIMIT_THRESHOLD = 3
REQUEST_TIMEOUT = 10 REQUEST_TIMEOUT = 10
APPNAME_DAYZ = "DayZ" APPNAME_DAYZ = "DayZ"
+7 -2
View File
@@ -1,6 +1,11 @@
# internal # internal
STEAM_PUBLISHED_FILES = "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/?format=json" STEAM_PUBLISHED_FILES = (
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?" "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?" BM_SERVERS = "https://api.battlemetrics.com/servers?"
GITHUB = "https://github.com/aclist" GITHUB = "https://github.com/aclist"
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest" GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"
+6 -5
View File
@@ -12,10 +12,10 @@ import dzgui.api.servers as Servers
from dzgui.api.steam import ( from dzgui.api.steam import (
connect, connect,
enqueue_mod,
get_remote_signatures, get_remote_signatures,
get_needs_update, get_needs_update,
load_to_menu, load_to_menu,
subscribe,
) )
from dzgui.api.mods import ( from dzgui.api.mods import (
@@ -30,6 +30,7 @@ from dzgui.const.constants import (
APPID_DAYZ_EXP, APPID_DAYZ_EXP,
APPNAME_DAYZ, APPNAME_DAYZ,
APPNAME_DAYZ_EXP_HUMAN, APPNAME_DAYZ_EXP_HUMAN,
RATE_LIMIT_THRESHOLD,
) )
from dzgui.const.enum import NotebookPage, Preferences from dzgui.const.enum import NotebookPage, Preferences
from dzgui.init.proc import is_dayz_running, is_steam_running from dzgui.init.proc import is_dayz_running, is_steam_running
@@ -294,15 +295,15 @@ class ConnectionManager:
self.controller.open_page(NotebookPage.SERVERS) self.controller.open_page(NotebookPage.SERVERS)
def _update_mods(self, raise_window: bool, menu_only: bool = False) -> None: 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() 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: for title, mod, stamp, size in self.missing_mods:
if self.controller.is_cancel_pending(): if self.controller.is_cancel_pending():
return return
enqueue_mod(self.client, mod, self.appid) subscribe(key, mod)
# NOTE: prevents rate limiting time.sleep(RATE_LIMIT_THRESHOLD)
time.sleep(3)
if raise_window is True: if raise_window is True:
logger.info("Bringing window to foreground") logger.info("Bringing window to foreground")
+26 -16
View File
@@ -1,17 +1,22 @@
import logging import logging
import shutil import time
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.api.steam import unsubscribe
from dzgui.api.mods import ( from dzgui.api.mods import (
get_delimited_mods, get_delimited_mods,
get_local_mod_path,
find_stale_mods, find_stale_mods,
_hash, _hash,
remove_stale_signatures, 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.const.enum import Preferences
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.model.model_factory import FastInsertListStore, ModelFactory from dzgui.model.model_factory import FastInsertListStore, ModelFactory
@@ -77,7 +82,7 @@ class ModManager:
total_mods = len(self.store) total_mods = len(self.store)
self.emitter.emit("mods_updated", msg, total_mods) self.emitter.emit("mods_updated", msg, total_mods)
def delete_mods(self) -> None: def unsub_mods(self) -> None:
sel = self.treeview.get_selection() sel = self.treeview.get_selection()
model, pathlist = sel.get_selected_rows() model, pathlist = sel.get_selected_rows()
# NOTE: reverse when multiple selection # NOTE: reverse when multiple selection
@@ -88,7 +93,7 @@ class ModManager:
continue continue
mod, _iter = res mod, _iter = res
mods.append((mod, _iter)) mods.append((mod, _iter))
self.delete_mods_on_system(mods) self.unsub_all_mods(mods)
def get_mod_from_tree_path( def get_mod_from_tree_path(
self, tree_path: Gtk.TreePath self, tree_path: Gtk.TreePath
@@ -101,24 +106,28 @@ class ModManager:
return mod, tree_iter return mod, tree_iter
@call_on_thread(dialogs.deleting_mods) @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: for mod, _iter in mods:
self.delete_single_mod(mod) self.unsub_atomic_mod(mod)
iters = [_iter for mod, _iter in mods] 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) 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) steam_path = Path(self.path)
mods_path = get_local_mod_path(steam_path)
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ) app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
md5 = _hash(mod) try:
symlink = app_path / md5 md5 = _hash(mod)
symlink.unlink() symlink = app_path / md5
shutil.rmtree(mods_path / mod) symlink.unlink()
except Exception as e:
logger.warning(e)
# NOTE: second pass to unlink DAYZ_EXP mods # NOTE: second pass to unlink DAYZ_EXP mods
# TODO: test this with working APPID_DAYZ_EXP installation # TODO: test this with working APPID_DAYZ_EXP installation
try: try:
@@ -127,8 +136,9 @@ class ModManager:
symlink.unlink() symlink.unlink()
except PeFile.AppNotInstalledError: except PeFile.AppNotInstalledError:
pass 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: if self.store is None:
return return
for _iter in iters: for _iter in iters: