diff --git a/pytorrent/routes/api.py b/pytorrent/routes/api.py
index 5526df2..51937b9 100644
--- a/pytorrent/routes/api.py
+++ b/pytorrent/routes/api.py
@@ -491,12 +491,17 @@ def trackers_summary():
if not profile:
return ok({"summary": {"hashes": {}, "trackers": [], "errors": [], "scanned": 0, "pending": 0}, "error": "No profile"})
try:
- # Note: Tracker summary uses the local torrent snapshot and refreshes only a small cache batch per request.
- scan_limit = min(250, max(0, int(request.args.get("scan_limit") or 80)))
+ # Note: Tracker summary returns cached data immediately; optional warmup scans rTorrent in the background for very large libraries.
+ scan_limit = min(250, max(0, int(request.args.get("scan_limit") or 0)))
+ bg_limit = min(250, max(1, int(request.args.get("bg_limit") or 80)))
+ warm = str(request.args.get("warm") or "").lower() in {"1", "true", "yes"}
hashes = [t.get("hash") for t in torrent_cache.snapshot(profile["id"]) if t.get("hash")]
prefs = preferences.get_preferences()
include_favicons = bool(prefs and prefs.get("tracker_favicons_enabled"))
- summary = tracker_cache.summary(profile, hashes, lambda h: rtorrent.torrent_trackers(profile, h), scan_limit=scan_limit, include_favicons=include_favicons)
+ loader = lambda h: rtorrent.torrent_trackers(profile, h)
+ summary = tracker_cache.summary(profile, hashes, loader, scan_limit=scan_limit, include_favicons=include_favicons)
+ if warm and int(summary.get("pending") or 0) > 0:
+ summary["warming"] = tracker_cache.warm_summary_cache(profile, hashes, loader, batch_size=bg_limit)
return ok({"summary": summary})
except Exception as exc:
return ok({"summary": {"hashes": {}, "trackers": [], "errors": [{"error": str(exc)}], "scanned": 0, "pending": 0}, "error": str(exc)})
diff --git a/pytorrent/services/tracker_cache.py b/pytorrent/services/tracker_cache.py
index 830ce1d..304396f 100644
--- a/pytorrent/services/tracker_cache.py
+++ b/pytorrent/services/tracker_cache.py
@@ -4,6 +4,7 @@ import json
import mimetypes
import re
import time
+import threading
import urllib.error
import urllib.parse
import urllib.request
@@ -18,6 +19,8 @@ FAVICON_CACHE_TTL_SECONDS = 7 * 24 * 60 * 60
TRACKER_SCAN_LIMIT = 80
FAVICON_DIR = BASE_DIR / "data" / "tracker_favicons"
PUBLIC_FAVICON_BASE = "/static/tracker_favicons"
+_TRACKER_SCAN_LOCKS: dict[int, threading.Lock] = {}
+_TRACKER_SCAN_LOCKS_GUARD = threading.Lock()
class _IconParser(HTMLParser):
@@ -159,6 +162,40 @@ def summary(profile: dict, hashes: list[str], loader, scan_limit: int = TRACKER_
+def _scan_lock(profile_id: int) -> threading.Lock:
+ with _TRACKER_SCAN_LOCKS_GUARD:
+ if profile_id not in _TRACKER_SCAN_LOCKS:
+ _TRACKER_SCAN_LOCKS[profile_id] = threading.Lock()
+ return _TRACKER_SCAN_LOCKS[profile_id]
+
+
+def warm_summary_cache(profile: dict, hashes: list[str], loader, batch_size: int = TRACKER_SCAN_LIMIT) -> bool:
+ """Start a non-blocking tracker cache warmup for large libraries."""
+ # Note: Tracker cache warming runs in one background thread per profile, so F5 returns cached data immediately instead of waiting for rTorrent scans.
+ profile_id = int(profile.get("id") or 0)
+ clean_hashes = [str(h or "").strip() for h in hashes if str(h or "").strip()]
+ if not profile_id or not clean_hashes:
+ return False
+ lock = _scan_lock(profile_id)
+ if lock.locked():
+ return False
+
+ def _worker():
+ if not lock.acquire(blocking=False):
+ return
+ try:
+ while True:
+ result = summary(profile, clean_hashes, loader, scan_limit=max(1, int(batch_size or TRACKER_SCAN_LIMIT)), include_favicons=False)
+ if int(result.get("pending") or 0) <= 0 or int(result.get("scanned_now") or 0) <= 0:
+ break
+ time.sleep(0.05)
+ finally:
+ lock.release()
+
+ threading.Thread(target=_worker, name=f"tracker-cache-warm-{profile_id}", daemon=True).start()
+ return True
+
+
def favicon_public_url(domain: str, enabled: bool = True, create: bool = False, force: bool = False) -> str:
"""Return the static URL for a cached tracker favicon, optionally creating or refreshing it first."""
# Note: Favicon files stay in data/tracker_favicons, but the browser loads them via the static/tracker_favicons symlink.
diff --git a/pytorrent/static/app.js b/pytorrent/static/app.js
index 260f644..6e7c81f 100644
--- a/pytorrent/static/app.js
+++ b/pytorrent/static/app.js
@@ -245,9 +245,9 @@
return ``;
}
function trackerFilterPlaceholder(){
- if(trackerSummaryStatus==='loading') return '