mirror of
https://github.com/aclist/dztui.git
synced 2026-10-30 14:42:54 +01:00
feat: unsubscribe from mods gracefully
This commit is contained in:
1 parent
179f92234a
commit
8d77d46eda
5 files changed
+76
-34
No files matched your search
+36
-11
@@ -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])
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
Reference in new issue
Block a user