From e8f72c6fe65542b7461b6f32f9649a71fec1d9bd Mon Sep 17 00:00:00 2001 From: pax Date: Sat, 4 Apr 2026 21:11:01 -0500 Subject: [PATCH] =?UTF-8?q?Add=20Network=20tab=20to=20settings=20=E2=80=94?= =?UTF-8?q?=20shows=20all=20connected=20hosts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Logs every outgoing connection (API requests and image downloads) with timestamps. Network tab in Settings shows all hosts contacted this session with request counts. No telemetry, just transparency. --- booru_viewer/core/api/base.py | 6 ++++++ booru_viewer/core/cache.py | 22 +++++++++++++++++++++- booru_viewer/gui/settings.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/booru_viewer/core/api/base.py b/booru_viewer/core/api/base.py index 8ea114a..036f517 100644 --- a/booru_viewer/core/api/base.py +++ b/booru_viewer/core/api/base.py @@ -9,6 +9,7 @@ from dataclasses import dataclass, field import httpx from ..config import USER_AGENT, DEFAULT_PAGE_SIZE +from ..cache import log_connection log = logging.getLogger("booru") @@ -53,9 +54,14 @@ class BooruClient(ABC): headers={"User-Agent": USER_AGENT}, follow_redirects=True, timeout=20.0, + event_hooks={"request": [self._log_request]}, ) return self._client + @staticmethod + async def _log_request(request: httpx.Request) -> None: + log_connection(str(request.url)) + async def close(self) -> None: if self._client and not self._client.is_closed: await self._client.aclose() diff --git a/booru_viewer/core/cache.py b/booru_viewer/core/cache.py index 40a518a..1297e1f 100644 --- a/booru_viewer/core/cache.py +++ b/booru_viewer/core/cache.py @@ -4,13 +4,32 @@ from __future__ import annotations import hashlib import zipfile +from collections import OrderedDict +from datetime import datetime from pathlib import Path +from urllib.parse import urlparse import httpx from PIL import Image from .config import cache_dir, thumbnails_dir, USER_AGENT +# Track all outgoing connections: {host: [timestamp, ...]} +_connection_log: OrderedDict[str, list[str]] = OrderedDict() + + +def log_connection(url: str) -> None: + host = urlparse(url).netloc + if host not in _connection_log: + _connection_log[host] = [] + _connection_log[host].append(datetime.now().strftime("%H:%M:%S")) + # Keep last 50 entries per host + _connection_log[host] = _connection_log[host][-50:] + + +def get_connection_log() -> dict[str, list[str]]: + return dict(_connection_log) + def _url_hash(url: str) -> str: return hashlib.sha256(url.encode()).hexdigest()[:16] @@ -110,7 +129,6 @@ async def download_image( local.unlink() # Remove corrupt cache entry # Extract referer from URL domain (needed for Gelbooru CDN etc.) - from urllib.parse import urlparse parsed = urlparse(url) # Map CDN hostnames back to the main site referer_host = parsed.netloc @@ -120,6 +138,8 @@ async def download_image( referer_host = "danbooru.donmai.us" referer = f"{parsed.scheme}://{referer_host}/" + log_connection(url) + own_client = client is None if own_client: client = httpx.AsyncClient( diff --git a/booru_viewer/gui/settings.py b/booru_viewer/gui/settings.py index c2cfd90..b5e4ae5 100644 --- a/booru_viewer/gui/settings.py +++ b/booru_viewer/gui/settings.py @@ -53,6 +53,7 @@ class SettingsDialog(QDialog): self._tabs.addTab(self._build_blacklist_tab(), "Blacklist") self._tabs.addTab(self._build_paths_tab(), "Paths") self._tabs.addTab(self._build_theme_tab(), "Theme") + self._tabs.addTab(self._build_network_tab(), "Network") # Bottom buttons btns = QHBoxLayout() @@ -338,6 +339,39 @@ class SettingsDialog(QDialog): layout.addStretch() return w + # -- Network tab -- + + def _build_network_tab(self) -> QWidget: + from ..core.cache import get_connection_log + w = QWidget() + layout = QVBoxLayout(w) + + layout.addWidget(QLabel( + "All hosts contacted this session. booru-viewer only connects\n" + "to the booru sites you configure — no telemetry or analytics." + )) + + self._net_list = QListWidget() + self._net_list.setAlternatingRowColors(True) + layout.addWidget(self._net_list) + + refresh_btn = QPushButton("Refresh") + refresh_btn.clicked.connect(self._refresh_network) + layout.addWidget(refresh_btn) + + self._refresh_network() + return w + + def _refresh_network(self) -> None: + from ..core.cache import get_connection_log + self._net_list.clear() + log = get_connection_log() + if not log: + self._net_list.addItem("No connections made yet") + return + for host, times in log.items(): + self._net_list.addItem(f"{host} ({len(times)} requests, last: {times[-1]})") + def _edit_custom_css(self) -> None: from PySide6.QtGui import QDesktopServices from PySide6.QtCore import QUrl