mirror of
https://github.com/aclist/dztui.git
synced 2026-11-05 17:45:10 +01:00
Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7582ab4d3b | ||
|
|
225a2a148e | ||
|
|
9117655ebd | ||
|
|
fdf92dca40 | ||
|
|
a97766855d | ||
|
|
3097701e98 | ||
|
|
895e08d228 | ||
|
|
08a6f698a5 | ||
|
|
78be8c93b7 | ||
|
|
8a2889d15d | ||
|
|
1a33f71716 | ||
|
|
596d3a59ac | ||
|
|
e7cef215c6 | ||
|
|
12daf041ff | ||
|
|
90ef6cbdbb | ||
|
|
2cc26b6573 | ||
|
|
0ec1c157f4 | ||
|
|
0cf6439538 | ||
|
|
e3c1ef59ee | ||
|
|
78144e9127 | ||
|
|
33b8dfba70 | ||
|
|
abf0060cac | ||
|
|
95daa9163b | ||
|
|
ff068e5396 | ||
|
|
80680b86b0 | ||
|
|
a4ac357c8f | ||
|
|
728b8b8242 | ||
|
|
3b51392d39 | ||
|
|
159e2da355 | ||
|
|
b83a333cea |
No files matched your search
+28
-3
@@ -1,15 +1,18 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from collections.abc import Iterator
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from warnings import deprecated
|
||||
|
||||
from dzgui.const.constants import APP_NAME
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
|
||||
class ACF:
|
||||
def __init__(self, file: Path) -> None:
|
||||
@@ -42,7 +45,7 @@ class ACF:
|
||||
def dequote(cls, s: str) -> str:
|
||||
return s.rstrip('"').lstrip('"')
|
||||
|
||||
def parse(self, lines: Iterator[list[str]]) -> dict[str, str]:
|
||||
def parse(self, lines: "Iterator[list[str]]") -> dict[str, str]:
|
||||
acf: dict[str, Any] = {}
|
||||
try:
|
||||
while True:
|
||||
@@ -69,6 +72,28 @@ class ACF:
|
||||
except StopIteration:
|
||||
return acf
|
||||
|
||||
def search_key(self, key: str) -> str:
|
||||
return str(self.dict["AppState"][key])
|
||||
|
||||
def date_to_str(self, epoch: int) -> str | None:
|
||||
if epoch == 0:
|
||||
return None
|
||||
return datetime.fromtimestamp(epoch).isoformat(" ")
|
||||
|
||||
def get_date_by_key(self, key: str) -> str | None:
|
||||
try:
|
||||
stamp = int(self.search_key(key))
|
||||
return self.date_to_str(stamp)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
return None
|
||||
|
||||
def get_last_updated(self) -> str | None:
|
||||
return self.get_date_by_key("LastUpdated")
|
||||
|
||||
def get_last_played(self) -> str | None:
|
||||
return self.get_date_by_key("LastPlayed")
|
||||
|
||||
def get_allows_downloads(self) -> int | None:
|
||||
try:
|
||||
# TODO: consider coercing strs to ints on initial import
|
||||
|
||||
+30
-3
@@ -12,6 +12,7 @@ from typing import Any, Generator, Union
|
||||
from warnings import deprecated
|
||||
|
||||
from dzgui.api.acf import ACF
|
||||
from dzgui.const.endpoints import VALIDATE
|
||||
from dzgui.init.prereqs import has_any_client
|
||||
from dzgui.init.prereqs import has_flatpak_client, has_steam_client
|
||||
from dzgui.const.constants import (
|
||||
@@ -316,10 +317,9 @@ def unsubscribe(key: str, mod: int) -> None:
|
||||
update_workshop(key, mod, UNSUB_ENDPOINT)
|
||||
|
||||
|
||||
@deprecated("Use subscribe()")
|
||||
def enqueue_mod(client: str, mod: str, appid: int) -> None:
|
||||
def validate_mod(client: str, mod: str) -> None:
|
||||
client_args = concat_bash_args(client)
|
||||
subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod])
|
||||
subprocess.Popen([*client_args, "+workshop_download_item", str(APPID_DAYZ), mod])
|
||||
|
||||
|
||||
@deprecated("Cf. https://github.com/ValveSoftware/steam-for-linux/issues/9672")
|
||||
@@ -389,6 +389,26 @@ def get_running_app() -> int | None:
|
||||
return None
|
||||
|
||||
|
||||
def get_last_updated(path: Path, appid: int) -> str | None:
|
||||
root_path = get_app_path(path, appid)
|
||||
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
|
||||
try:
|
||||
return ACF(acf).get_last_updated()
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
return None
|
||||
|
||||
|
||||
def get_last_played(path: Path, appid: int) -> str | None:
|
||||
root_path = get_app_path(path, appid)
|
||||
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
|
||||
try:
|
||||
return ACF(acf).get_last_played()
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
return None
|
||||
|
||||
|
||||
def get_app_allows_downloads(path: Path, appid: int) -> bool:
|
||||
try:
|
||||
root_path = get_app_path(path, appid)
|
||||
@@ -500,3 +520,10 @@ def get_app_name(appid: int) -> str | None:
|
||||
except Exception as e:
|
||||
logger.debug(e)
|
||||
return None
|
||||
|
||||
|
||||
@deprecated("Currently unused")
|
||||
def validate_dayz(client: str) -> None:
|
||||
client_args = concat_bash_args(client)
|
||||
uri = VALIDATE + str(APPID_DAYZ)
|
||||
subprocess.Popen([*client_args, uri])
|
||||
+102
-3
@@ -3,6 +3,9 @@ import logging
|
||||
import re
|
||||
import requests
|
||||
import shutil
|
||||
import socket
|
||||
import sqlite3
|
||||
import time
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -22,15 +25,16 @@ def find_download_url(url: str) -> str | None:
|
||||
return str(urls[0])
|
||||
|
||||
|
||||
def get_ipdb(ips_path: Path) -> None:
|
||||
def get_ipdb(db_path: Path) -> None:
|
||||
try:
|
||||
url = find_download_url(DB_IP)
|
||||
if url is None:
|
||||
return
|
||||
|
||||
ips_path = db_path.with_suffix(".csv")
|
||||
month_file = db_path.parent / ".month"
|
||||
date = find_date(url)
|
||||
month_file = ips_path.parent / ".month"
|
||||
if ips_path.exists() and month_file.exists():
|
||||
if db_path.exists() and month_file.exists():
|
||||
old_date = month_file.read_text().rstrip("\n")
|
||||
if old_date == date:
|
||||
logger.info(f"IP DB date matches: {date}")
|
||||
@@ -45,6 +49,9 @@ def get_ipdb(ips_path: Path) -> None:
|
||||
logger.info("Stripping IPv6 records")
|
||||
strip_ipv6(ips_path)
|
||||
|
||||
create_table(db_path, ips_path)
|
||||
ips_path.unlink()
|
||||
|
||||
with open(month_file, "w") as f:
|
||||
f.write(date)
|
||||
logger.info(f"Wrote {date} to {month_file}")
|
||||
@@ -108,3 +115,95 @@ def strip_ipv6(path: Path) -> None:
|
||||
out.write(merged)
|
||||
path.unlink()
|
||||
alt_path.rename(path)
|
||||
|
||||
|
||||
def ip_to_int(ip: str) -> int:
|
||||
return int.from_bytes(socket.inet_aton(ip), "big")
|
||||
|
||||
|
||||
def marshal_rows(ip_file: Path) -> list[tuple[int, int, str, str]]:
|
||||
YIELD = 0.003
|
||||
SLEEP = 0.001
|
||||
with open(ip_file, "r") as f:
|
||||
rows: list[tuple[int, int, str, str]] = []
|
||||
# NOTE: yield to GIL periodically to reduce load on spinner
|
||||
last_yield = time.monotonic()
|
||||
for row in f:
|
||||
start, end, lat, lon = row.rstrip("\n").split(",")
|
||||
int_start = ip_to_int(start)
|
||||
int_end = ip_to_int(end)
|
||||
rows.append((int_start, int_end, lat, lon))
|
||||
|
||||
now = time.monotonic()
|
||||
if (now - last_yield) >= YIELD:
|
||||
time.sleep(SLEEP)
|
||||
last_yield = time.monotonic()
|
||||
return rows
|
||||
|
||||
|
||||
def create_table(db_file: Path, ip_file: Path) -> None:
|
||||
create = """
|
||||
CREATE TABLE IF NOT EXISTS ips (
|
||||
start_range INTEGER PRIMARY KEY,
|
||||
end_range INTEGER,
|
||||
latitude REAL,
|
||||
longitude REAL
|
||||
) WITHOUT ROWID;
|
||||
"""
|
||||
insert = """
|
||||
INSERT INTO ips
|
||||
(start_range, end_range, latitude, longitude)
|
||||
values(?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
if db_file.exists():
|
||||
db_file.unlink()
|
||||
|
||||
conn = sqlite3.connect(":memory:")
|
||||
c = conn.cursor()
|
||||
|
||||
try:
|
||||
c.execute(create)
|
||||
rows = marshal_rows(ip_file)
|
||||
c.executemany(insert, rows)
|
||||
logger.info(f"Inserted {len(rows)} IP records into database")
|
||||
conn.commit()
|
||||
with sqlite3.connect(db_file) as dest:
|
||||
conn.backup(dest)
|
||||
logger.info(f"Serialized IP records to database: '{db_file}'")
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
|
||||
def read_from_table(db_path: Path, ip: str) -> tuple[float, float]:
|
||||
if not db_path.exists():
|
||||
raise FileNotFoundError(f"No file found at the location '{db_path}")
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
int_ip = ip_to_int(ip)
|
||||
|
||||
query = """
|
||||
SELECT end_range, latitude, longitude
|
||||
FROM ips
|
||||
WHERE start_range <= ?
|
||||
ORDER BY start_range DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
index = """
|
||||
CREATE INDEX IF NOT EXISTS start_index
|
||||
ON ips(start_range);
|
||||
"""
|
||||
try:
|
||||
c.execute(index)
|
||||
conn.commit()
|
||||
c.execute(query, (int_ip,))
|
||||
row = c.fetchone()
|
||||
end, lat, lon = row
|
||||
conn.close()
|
||||
return lat, lon
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
raise e
|
||||
+3
-3
@@ -22,7 +22,7 @@ class Xdg:
|
||||
version: Path
|
||||
system: Path
|
||||
debug: Path
|
||||
ips: Path
|
||||
db: Path
|
||||
filters: Path
|
||||
shortcut: Path
|
||||
|
||||
@@ -88,7 +88,7 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
||||
resolution = state / "dzg.res.json"
|
||||
history = state / "dzg.history"
|
||||
versions = state / "dzg.versions"
|
||||
ips = state / "ips.csv"
|
||||
db = state / "ips.db"
|
||||
filters = state / "dzg.filters.json"
|
||||
|
||||
shortcut = share / APP_NAME_LOWER
|
||||
@@ -102,7 +102,7 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
||||
versions,
|
||||
system,
|
||||
debug,
|
||||
ips,
|
||||
db,
|
||||
filters,
|
||||
shortcut,
|
||||
)
|
||||
@@ -18,6 +18,7 @@ COORDS_API = "http://ip-api.com/json"
|
||||
# steam URIs
|
||||
WORKSHOP = "steam://url/CommunityFilePage/"
|
||||
OPEN_URL = "steam://openurl/"
|
||||
VALIDATE = "steam://validate/"
|
||||
|
||||
# user facing
|
||||
GITHUB_PAGES = "https://aclist.github.io/dzgui/index"
|
||||
|
||||
+6
-1
@@ -157,6 +157,7 @@ class ContextMenu(EnumWithAttrs):
|
||||
COPY_SERVER_IP = {"label": strings.copy_ip}
|
||||
COPY_SERVER_NAME = {"label": strings.copy_name}
|
||||
UNSUB_MOD = {"label": strings.unsub_mod}
|
||||
VALIDATE_MOD = {"label": strings.validate_mod}
|
||||
OPEN_WORKSHOP = {"label": strings.open_workshop}
|
||||
REFRESH_PLAYERS = {"label": strings.refresh_players}
|
||||
REMOVE_HISTORY = {"label": strings.remove_history}
|
||||
@@ -172,7 +173,7 @@ class ContextMenuGroup(Enum):
|
||||
"""
|
||||
|
||||
SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,)
|
||||
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD)
|
||||
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD, ContextMenu.VALIDATE_MOD)
|
||||
MOD_OFFLINE = (None,)
|
||||
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
|
||||
SERVER_BROWSER = (
|
||||
@@ -246,6 +247,10 @@ class ModButton(EnumWithAttrs):
|
||||
"label": strings.mod_panel.select_stale,
|
||||
"tooltip": strings.mod_panel.select_stale_tooltip,
|
||||
}
|
||||
VALIDATE = {
|
||||
"label": strings.mod_panel.validate,
|
||||
"tooltip": strings.mod_panel.validate_tooltip,
|
||||
}
|
||||
|
||||
|
||||
class ButtonType(EnumWithAttrs):
|
||||
|
||||
@@ -102,6 +102,16 @@ class Controller(GObject.GObject):
|
||||
except AttributeError:
|
||||
logger.critical(f"{attr} is not a valid AppNavigation attribute.")
|
||||
|
||||
def validate_mods(
|
||||
self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None
|
||||
) -> None:
|
||||
if treeview is None:
|
||||
view = self.mediator.modtreeview
|
||||
else:
|
||||
view = treeview
|
||||
mod_man = view.get_mod_man()
|
||||
mod_man.validate_mods()
|
||||
|
||||
def get_prefs(self) -> UserPrefs:
|
||||
return self.prefs
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
def get_local_coords(path: "Path") -> Coords | None:
|
||||
def get_local_coords(db_path: "Path") -> Coords | None:
|
||||
try:
|
||||
my_ip = get_local_ip()
|
||||
return get_coords(path, my_ip)
|
||||
return get_coords(db_path, my_ip)
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
return None
|
||||
+1
-1
Submodule dzgui/lib/dayzquery updated: 07483b88ed...fe413df8e8.
@@ -75,10 +75,14 @@ class ContextMenuManager:
|
||||
|
||||
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
|
||||
match action:
|
||||
case ContextMenu.UNSUB_MOD:
|
||||
self.controller.unsub_mods(self.treeview)
|
||||
case ContextMenu.OPEN_WORKSHOP:
|
||||
self.open_mod_page()
|
||||
case ContextMenu.UNSUB_MOD:
|
||||
self.controller.unsub_mods(self.treeview)
|
||||
case ContextMenu.VALIDATE_MOD:
|
||||
self.controller.validate_mods(self.treeview)
|
||||
case _:
|
||||
pass
|
||||
|
||||
if isinstance(self.treeview, LogTreeView):
|
||||
match action:
|
||||
|
||||
+26
-2
@@ -4,7 +4,7 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dzgui.api.steam import unsubscribe
|
||||
from dzgui.api.steam import unsubscribe, validate_mod
|
||||
from dzgui.api.mods import (
|
||||
get_delimited_mods,
|
||||
find_stale_mods,
|
||||
@@ -85,7 +85,7 @@ class ModManager:
|
||||
total_mods = len(self.store)
|
||||
self.emitter.emit("mods_updated", msg, total_mods)
|
||||
|
||||
def unsub_mods(self) -> None:
|
||||
def get_selected_mods(self) -> list[tuple[str, Gtk.TreeIter]]:
|
||||
sel = self.treeview.get_selection()
|
||||
model, pathlist = sel.get_selected_rows()
|
||||
# NOTE: reverse when multiple selection
|
||||
@@ -96,6 +96,15 @@ class ModManager:
|
||||
continue
|
||||
mod, _iter = res
|
||||
mods.append((mod, _iter))
|
||||
return mods
|
||||
|
||||
def validate_mods(self) -> None:
|
||||
mods = self.get_selected_mods()
|
||||
self.thread_man.set_job_count(len(mods))
|
||||
self.validate_all_mods(mods)
|
||||
|
||||
def unsub_mods(self) -> None:
|
||||
mods = self.get_selected_mods()
|
||||
self.thread_man.set_job_count(len(mods))
|
||||
self.unsub_all_mods(mods)
|
||||
|
||||
@@ -109,6 +118,21 @@ class ModManager:
|
||||
mod = model.get_value(tree_iter, 2)
|
||||
return mod, tree_iter
|
||||
|
||||
def validate_atomic_mod(self, mod: str) -> None:
|
||||
# NOTE: propagation to dayz exp will happen during
|
||||
# subsequent symlink phase on connect
|
||||
client = self.controller.query_config(Preferences.CLIENT)
|
||||
validate_mod(client, mod)
|
||||
|
||||
@call_on_thread(dialogs.validating_mods)
|
||||
def validate_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
|
||||
for mod, _iter in mods:
|
||||
self.validate_atomic_mod(mod)
|
||||
self.thread_man.increment_dialog()
|
||||
time.sleep(API_RATE_LIMIT)
|
||||
func = StoredFunc(lambda: None)
|
||||
self.thread_man.set_cleanup_func(func)
|
||||
|
||||
@call_on_thread(dialogs.deleting_mods)
|
||||
def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
|
||||
for mod, _iter in mods:
|
||||
|
||||
+30
-7
@@ -201,19 +201,43 @@ class ServerModelManager:
|
||||
|
||||
@call_on_thread(dialog.querying)
|
||||
def add_by_ip(self, addr: str) -> None:
|
||||
res = Servers.query_by_ip(addr)
|
||||
self._parse_single_record(res)
|
||||
self.preserve_on_fail = True
|
||||
try:
|
||||
res = Servers.query_by_ip(addr)
|
||||
self._parse_single_record(res)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
self.thread_man.set_cleanup_func(
|
||||
StoredFunc(self._cleanup_on_failure), destroy_first=True
|
||||
)
|
||||
return
|
||||
|
||||
@call_on_thread(dialog.querying)
|
||||
def add_by_record(self, record: Servers.Record) -> None:
|
||||
self.preserve_on_fail = True
|
||||
# NOTE: rationale: a raw Record as shown in server browser may resolve to a different IP
|
||||
res = Servers.query_by_record(record)
|
||||
self._parse_single_record(res)
|
||||
try:
|
||||
res = Servers.query_by_record(record)
|
||||
self._parse_single_record(res)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
self.thread_man.set_cleanup_func(
|
||||
StoredFunc(self._cleanup_on_failure), destroy_first=True
|
||||
)
|
||||
return
|
||||
|
||||
@call_on_thread(dialog.querying)
|
||||
def remove_by_record(self, record: Servers.Record) -> None:
|
||||
res = Servers.query_by_record(record)
|
||||
self._parse_single_record(res, delete=True)
|
||||
self.preserve_on_fail = True
|
||||
try:
|
||||
res = Servers.query_by_record(record)
|
||||
self._parse_single_record(res, delete=True)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
self.thread_man.set_cleanup_func(
|
||||
StoredFunc(self._cleanup_on_failure), destroy_first=True
|
||||
)
|
||||
return
|
||||
|
||||
def update_history(
|
||||
self,
|
||||
@@ -280,7 +304,6 @@ class ServerModelManager:
|
||||
proxy_man.update_playercount(self.playercount)
|
||||
|
||||
def _parse_single_record(self, response: "A2SInfo", delete: bool = False) -> None:
|
||||
self.preserve_on_fail = True
|
||||
try:
|
||||
row = response.as_row()
|
||||
except Exception as e:
|
||||
|
||||
@@ -9,6 +9,7 @@ update_success = "Updated successfully. Please exit and relaunch."
|
||||
load_error_lan = "Failed to find any servers on your network.\nCheck the server query port or your firewall settings."
|
||||
fetching_mods = "Fetching mod metadata"
|
||||
deleting_mods = "Unsubscribing mods"
|
||||
validating_mods = "Validating mods"
|
||||
scanning_mods = "Scanning mods"
|
||||
parsing_mods = "Parsing mods"
|
||||
|
||||
|
||||
+3
-2
@@ -80,8 +80,9 @@ class CalcDist:
|
||||
if local is None:
|
||||
return None
|
||||
try:
|
||||
remote = get_coords(prefs.paths.ips, remote_ip)
|
||||
except Exception:
|
||||
remote = get_coords(prefs.paths.db, remote_ip)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
return None
|
||||
|
||||
haversine = Haversine(local.lat, local.lon, remote.lat, remote.lon)
|
||||
|
||||
+14
-45
@@ -1,4 +1,5 @@
|
||||
import a2s
|
||||
from ipaddress import ip_address
|
||||
import requests
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -7,6 +8,7 @@ from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dzgui.api.servers import Record
|
||||
from dzgui.config.ipdb import read_from_table
|
||||
from dzgui.const.constants import REQUEST_TIMEOUT
|
||||
from dzgui.const.endpoints import IP_ECHO
|
||||
|
||||
@@ -33,54 +35,21 @@ def ip2list(ip: str) -> list:
|
||||
return [int(digit) for digit in ip.split(".")]
|
||||
|
||||
|
||||
# TODO: tests
|
||||
def is_reserved(digits: list) -> bool:
|
||||
reserved = [0, 10, 127]
|
||||
|
||||
if digits[0] in reserved:
|
||||
return True
|
||||
if digits[0] == 192 and digits[1] == 168:
|
||||
return True
|
||||
return False
|
||||
def is_usable(ip: str) -> bool:
|
||||
address = ip_address(ip)
|
||||
if address.is_reserved or address.is_private:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_coords(ips: "Path", ip: str) -> Coords:
|
||||
split = ip2list(ip)
|
||||
|
||||
if is_reserved(split):
|
||||
def get_coords(db_path: "Path", ip: str) -> Coords:
|
||||
if not is_usable(ip):
|
||||
raise ReservedIPError(f"{ip} is a reserved IP address.")
|
||||
|
||||
prefix = f"^{split[0]}.{split[1]}."
|
||||
|
||||
if shutil.which("rg") is not None:
|
||||
args = ["/usr/bin/rg", prefix, str(ips)]
|
||||
else:
|
||||
args = ["/usr/bin/grep", "-E", prefix, str(ips)]
|
||||
proc = subprocess.run(args, capture_output=True, text=True)
|
||||
|
||||
if proc.returncode != 0:
|
||||
raise GeolocationError("Failed to split records")
|
||||
|
||||
ip_list = proc.stdout.splitlines()
|
||||
|
||||
for address in ip_list:
|
||||
fields = address.split(",")
|
||||
upper = fields[1]
|
||||
|
||||
upper_digits = ip2list(upper)
|
||||
if ip == upper:
|
||||
return Coords(float(fields[-2]), float(fields[-1]))
|
||||
|
||||
if split[2] > upper_digits[2]:
|
||||
continue
|
||||
|
||||
if split[2] == upper_digits[2]:
|
||||
if split[3] > upper_digits[3]:
|
||||
continue
|
||||
|
||||
if split[2] <= upper_digits[2]:
|
||||
return Coords(float(fields[-2]), float(fields[-1]))
|
||||
raise GeolocationError("No matching records found")
|
||||
try:
|
||||
lat, lon = read_from_table(db_path, ip)
|
||||
return Coords(lat, lon)
|
||||
except Exception:
|
||||
raise GeolocationError("No matching records found")
|
||||
|
||||
|
||||
def get_local_ip() -> str:
|
||||
|
||||
@@ -31,6 +31,7 @@ show_details = "Show server details"
|
||||
refresh_players = "Refresh player count"
|
||||
open_workshop = "Open in Steam Workshop"
|
||||
unsub_mod = "Unsubscribe mod"
|
||||
validate_mod = "Validate mod"
|
||||
copy_name = "Copy name to clipboard"
|
||||
copy_ip = "Copy IP to clipboard"
|
||||
copy_log = "Copy record(s) to clipboard"
|
||||
@@ -200,6 +201,8 @@ class ModPanelStrings:
|
||||
clear_highlights: str
|
||||
select_stale: str
|
||||
select_stale_tooltip: str
|
||||
validate: str
|
||||
validate_tooltip: str
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
@@ -319,6 +322,8 @@ mod_panel = ModPanelStrings(
|
||||
clear_highlights="Clears highlights and reverts the table to a default state",
|
||||
select_stale="Select stale",
|
||||
select_stale_tooltip="Only selects highlighted stale mods",
|
||||
validate="Validate selected",
|
||||
validate_tooltip="Validates selected mods and updates them if necessary",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -313,6 +313,8 @@ class FilterPanel(Gtk.Box):
|
||||
return True
|
||||
|
||||
def toggle_check(self, digit: int) -> None:
|
||||
if not self.button_grid.is_sensitive():
|
||||
return
|
||||
checks = self.button_grid.get_checkboxes()
|
||||
check = checks[digit]
|
||||
state = check.get_active()
|
||||
|
||||
@@ -50,6 +50,7 @@ class ModSelectionPanel(Gtk.Box):
|
||||
ModButton.SELECT_ALL,
|
||||
ModButton.UNSELECT_ALL,
|
||||
ModButton.UNSUB_SELECTED,
|
||||
ModButton.VALIDATE,
|
||||
)
|
||||
for button in buttons:
|
||||
b = EnumeratedModButton(button)
|
||||
@@ -114,3 +115,5 @@ class ModSelectionPanel(Gtk.Box):
|
||||
self.swap_sensitive(False)
|
||||
case ModButton.SELECT_STALE:
|
||||
self.controller.select_colorized()
|
||||
case ModButton.VALIDATE:
|
||||
self.controller.validate_mods()
|
||||
@@ -132,9 +132,9 @@ class BootDialog(ColorAwareApp, Gtk.Dialog): # type: ignore
|
||||
preboot.signatures,
|
||||
False,
|
||||
),
|
||||
(StoredFunc(get_ipdb, self.xdg.ips), preboot.geo, False),
|
||||
(StoredFunc(get_ipdb, self.xdg.db), preboot.geo, False),
|
||||
# TODO: handle IP DB failure and use coords fallback
|
||||
(StoredFunc(get_local_coords, self.xdg.ips), preboot.coords, True),
|
||||
(StoredFunc(get_local_coords, self.xdg.db), preboot.coords, True),
|
||||
(StoredFunc(check_updates, self.version), preboot.updates, True),
|
||||
]
|
||||
self.results: list[Any] = []
|
||||
|
||||
@@ -2,6 +2,7 @@ from pathlib import Path
|
||||
from typing import Callable, TYPE_CHECKING
|
||||
|
||||
from dzgui.api import pefile as PeFile
|
||||
from dzgui.api.steam import get_last_updated
|
||||
|
||||
from dzgui.config import query
|
||||
from dzgui.const.constants import (
|
||||
@@ -340,8 +341,10 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
[LeftLabel(options.flatpak_label), self.flatpak_toggle],
|
||||
)
|
||||
|
||||
self.dayz_version_label = Gtk.Label(label=strings.null)
|
||||
self.dayz_exp_version_label = Gtk.Label(label=strings.null)
|
||||
self.dayz_version_label = Gtk.Label(label=strings.null, halign=Gtk.Align.START)
|
||||
self.dayz_exp_version_label = Gtk.Label(
|
||||
label=strings.null, halign=Gtk.Align.START
|
||||
)
|
||||
|
||||
version_rows = [
|
||||
[LeftLabel(APPNAME_DAYZ), self.dayz_version_label],
|
||||
@@ -467,10 +470,18 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ)
|
||||
if dayz_version is None:
|
||||
dayz_version = strings.null
|
||||
else:
|
||||
last_updated = get_last_updated(steam_path, APPID_DAYZ)
|
||||
if last_updated is not None:
|
||||
dayz_version += f" (updated at {last_updated})"
|
||||
|
||||
dayz_exp_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ_EXP)
|
||||
if dayz_exp_version is None:
|
||||
dayz_exp_version = strings.null
|
||||
else:
|
||||
last_updated = get_last_updated(steam_path, APPID_DAYZ_EXP)
|
||||
if last_updated is not None:
|
||||
dayz_exp_version += f" (updated at {last_updated})"
|
||||
|
||||
self.dayz_version_label.set_text(dayz_version)
|
||||
self.dayz_exp_version_label.set_text(dayz_exp_version)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"AppState"
|
||||
{
|
||||
"appid" "444"
|
||||
"Universe" "1"
|
||||
"name" "TEST APP"
|
||||
"StateFlags" "4"
|
||||
"installdir" "TESTAPP"
|
||||
"LastUpdated" "1792595373"
|
||||
"LastPlayed" "0"
|
||||
"SizeOnDisk" "0"
|
||||
"StagingSize" "0"
|
||||
"buildid" "0"
|
||||
"LastOwner" "0"
|
||||
"DownloadType" "2"
|
||||
"UpdateResult" "0"
|
||||
"BytesToDownload" "0"
|
||||
"BytesDownloaded" "0"
|
||||
"BytesToStage" "0"
|
||||
"BytesStaged" "0"
|
||||
"TargetBuildID" "0"
|
||||
"AutoUpdateBehavior" "0"
|
||||
"AllowOtherDownloadsWhileRunning" "0"
|
||||
"ScheduledAutoUpdate" "0"
|
||||
"FullValidateAfterNextUpdate" "1"
|
||||
"InstalledDepots"
|
||||
{
|
||||
"3330"
|
||||
{
|
||||
"manifest" "0"
|
||||
"size" "0"
|
||||
}
|
||||
}
|
||||
"SharedDepots"
|
||||
{
|
||||
"228983" "228980"
|
||||
"228985" "228980"
|
||||
"228988" "228980"
|
||||
"228990" "228980"
|
||||
"229004" "228980"
|
||||
}
|
||||
"UserConfig"
|
||||
{
|
||||
"language" "english"
|
||||
"platform_override_dest" "linux"
|
||||
"platform_override_source" "windows"
|
||||
}
|
||||
"MountedConfig"
|
||||
{
|
||||
"language" "english"
|
||||
"platform_override_dest" "linux"
|
||||
"platform_override_source" "windows"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"AppState"
|
||||
{
|
||||
"appid" "555"
|
||||
"Universe" "1"
|
||||
"name" "TEST APP"
|
||||
"StateFlags" "4"
|
||||
"installdir" "TESTAPP"
|
||||
"LastUpdated" "0"
|
||||
"LastPlayed" "0"
|
||||
"SizeOnDisk" "0"
|
||||
"StagingSize" "0"
|
||||
"buildid" "0"
|
||||
"LastOwner" "0"
|
||||
"DownloadType" "2"
|
||||
"UpdateResult" "0"
|
||||
"BytesToDownload" "0"
|
||||
"BytesDownloaded" "0"
|
||||
"BytesToStage" "0"
|
||||
"BytesStaged" "0"
|
||||
"TargetBuildID" "0"
|
||||
"AutoUpdateBehavior" "0"
|
||||
"AllowOtherDownloadsWhileRunning" "0"
|
||||
"ScheduledAutoUpdate" "0"
|
||||
"FullValidateAfterNextUpdate" "1"
|
||||
"InstalledDepots"
|
||||
{
|
||||
"3330"
|
||||
{
|
||||
"manifest" "0"
|
||||
"size" "0"
|
||||
}
|
||||
}
|
||||
"SharedDepots"
|
||||
{
|
||||
"228983" "228980"
|
||||
"228985" "228980"
|
||||
"228988" "228980"
|
||||
"228990" "228980"
|
||||
"229004" "228980"
|
||||
}
|
||||
"UserConfig"
|
||||
{
|
||||
"language" "english"
|
||||
"platform_override_dest" "linux"
|
||||
"platform_override_source" "windows"
|
||||
}
|
||||
"MountedConfig"
|
||||
{
|
||||
"language" "english"
|
||||
"platform_override_dest" "linux"
|
||||
"platform_override_source" "windows"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import pytest
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from pathlib import Path
|
||||
|
||||
from dzgui.api.steam import get_app_allows_downloads
|
||||
from dzgui.api.steam import get_app_allows_downloads, get_last_updated
|
||||
from tests.fixtures import fixture_path
|
||||
|
||||
pytestmark = pytest.mark.apitest
|
||||
@@ -43,6 +43,14 @@ def patch_api() -> None:
|
||||
mp.undo()
|
||||
|
||||
|
||||
def test_no_last_update(steam_path) -> None:
|
||||
assert get_last_updated(steam_path, 555) is None
|
||||
|
||||
|
||||
def test_last_update(steam_path) -> None:
|
||||
assert get_last_updated(steam_path, 444) == "2026-10-22 00:09:33"
|
||||
|
||||
|
||||
def test_app_allows_downloads(steam_path) -> None:
|
||||
assert get_app_allows_downloads(steam_path, 111) is True
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ def keys():
|
||||
|
||||
# TODO: use a static fixture instead of system config
|
||||
@pytest.fixture
|
||||
@pytest.mark.realconfig
|
||||
def config():
|
||||
paths = get_xdg_paths()
|
||||
xdg = parse_filepaths(paths)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import configparser
|
||||
import pytest
|
||||
|
||||
from dzgui.const.constants import FLATPAK_APPID, GTK_APP_ID
|
||||
from dzgui.const.constants import FLATPAK_APPID, GTK_APP_ID, TMP_PATH
|
||||
from dzgui.config.xdg import get_xdg_paths
|
||||
from dzgui.util.flatpak import apply_overrides
|
||||
|
||||
@@ -38,6 +38,8 @@ def test_session_bus(override, config) -> None:
|
||||
|
||||
|
||||
def test_paths(override, xdg_paths, config) -> None:
|
||||
expect = xdg_paths
|
||||
expect: list[str] = []
|
||||
expect.append(TMP_PATH)
|
||||
expect.extend([*xdg_paths.values()])
|
||||
paths = config[PATHS_KEY][PATHS_SUBKEY].split(";")
|
||||
assert all(str(path) in paths for path in expect.values())
|
||||
assert all(str(path) in paths for path in expect)
|
||||
+44
-7
@@ -1,16 +1,40 @@
|
||||
from dzgui.config.ipdb import get_ipdb
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sqlite3
|
||||
import pytest
|
||||
import tempfile
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from dzgui.const.endpoints import DB_IP
|
||||
from dzgui.config.ipdb import (
|
||||
create_table,
|
||||
find_download_url,
|
||||
serialize,
|
||||
strip_ipv6,
|
||||
unzip,
|
||||
)
|
||||
|
||||
|
||||
def get_db_len(path: Path) -> int:
|
||||
conn = sqlite3.connect(path)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT COUNT(*) from ips")
|
||||
db_len = c.fetchone()[0]
|
||||
return db_len
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def csv() -> Path:
|
||||
csv = Path(tempfile.NamedTemporaryFile().name)
|
||||
url = find_download_url(DB_IP)
|
||||
tmp = serialize(url)
|
||||
unzip(tmp, csv)
|
||||
strip_ipv6(csv)
|
||||
return csv
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_stripping():
|
||||
d = tempfile.TemporaryDirectory()
|
||||
path = Path(d.name).joinpath("ips.csv")
|
||||
get_ipdb(path)
|
||||
with open(path, "rb") as f:
|
||||
def test_stripping(csv):
|
||||
with open(csv, "rb") as f:
|
||||
first = f.readline().decode().rstrip()
|
||||
try:
|
||||
f.seek(-2, os.SEEK_END)
|
||||
@@ -25,3 +49,16 @@ def test_stripping():
|
||||
|
||||
assert first_els[0] == "0.0.0.0"
|
||||
assert last_els[0] == "224.0.0.0"
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_row_parity(csv) -> None:
|
||||
rows = csv.read_text().splitlines()
|
||||
|
||||
db = Path(tempfile.NamedTemporaryFile().name)
|
||||
create_table(db, csv)
|
||||
|
||||
csv_len = len(rows)
|
||||
db_len = get_db_len(db)
|
||||
|
||||
assert csv_len == db_len
|
||||
@@ -0,0 +1,47 @@
|
||||
import pytest
|
||||
|
||||
from dzgui.model.model_factory import FastInsertListStore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def heterogeneous_list() -> None:
|
||||
return [["a", 0, True], ["b", 1, False]]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mismatched_length_list() -> None:
|
||||
return [["a", 0, True], ["b", 1, False, 0]]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def extended_list(heterogeneous_list) -> None:
|
||||
fs = FastInsertListStore(str, int, bool)
|
||||
fs.extend(heterogeneous_list)
|
||||
return fs
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def appended_list(heterogeneous_list) -> None:
|
||||
fs = FastInsertListStore(str, int, bool)
|
||||
for row in heterogeneous_list:
|
||||
fs.append(row)
|
||||
return fs
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target", ["extended_list", "appended_list"])
|
||||
def test_list_parity(heterogeneous_list, request, target) -> None:
|
||||
target = request.getfixturevalue(target)
|
||||
extracted = [[el for el in row] for row in target]
|
||||
assert heterogeneous_list == extracted
|
||||
|
||||
|
||||
def test_type_disparity(heterogeneous_list) -> None:
|
||||
with pytest.raises(TypeError):
|
||||
fs = FastInsertListStore(str, bool, bool)
|
||||
fs.extend(heterogeneous_list)
|
||||
|
||||
|
||||
def test_length_disparity(mismatched_length_list) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
fs = FastInsertListStore(str, int, bool)
|
||||
fs.extend(mismatched_length_list)
|
||||
Reference in new issue
Block a user