This commit is contained in:
Mateusz Gruszczyński
2026-05-08 19:59:38 +02:00
parent 3eee5be37a
commit 96e17d4b63
3 changed files with 50 additions and 8 deletions

View File

@@ -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)})

View File

@@ -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.

View File

@@ -245,9 +245,9 @@
return `<img class="tracker-favicon" src="${esc(src)}" alt="" loading="lazy" data-fallback-src="${esc(fallback)}" onerror="if(this.dataset.retry!=='1'){this.dataset.retry='1';this.src=this.dataset.fallbackSrc;}else{this.classList.add('d-none')}"><i class="fa-solid fa-bullseye tracker-fallback-icon"></i>`;
}
function trackerFilterPlaceholder(){
if(trackerSummaryStatus==='loading') return '<div class="tracker-filter-empty"><span class="spinner-border spinner-border-xs"></span> Loading trackers...</div>';
if(trackerSummaryStatus==='loading') return '<div class="tracker-filter-empty"><span class="spinner-border spinner-border-xs"></span> Loading cached trackers...</div>';
if(trackerSummaryStatus==='error') return '<div class="tracker-filter-empty text-warning"><i class="fa-solid fa-triangle-exclamation"></i> Tracker list unavailable</div>';
if(Number(trackerSummary.pending||0)) return `<div class="tracker-filter-empty"><span class="spinner-border spinner-border-xs"></span> Scanning cache: ${esc(trackerSummary.cached||0)}/${esc(trackerSummary.scanned||0)}</div>`;
if(Number(trackerSummary.pending||0)) return `<div class="tracker-filter-empty"><span class="spinner-border spinner-border-xs"></span> Tracker cache: ${esc(trackerSummary.cached||0)}/${esc(trackerSummary.scanned||0)}</div>`;
if(hasTorrentSnapshot && torrents.size) return '<div class="tracker-filter-empty">No trackers found</div>';
return '<div class="tracker-filter-empty">Waiting for torrents...</div>';
}
@@ -273,15 +273,15 @@
renderTrackerFilters();
try{
// Note: Nie wysyłamy 13k hashy w URL; backend bierze lokalny snapshot i doczytuje cache małymi porcjami.
const j=await (await fetch('/api/trackers/summary?scan_limit=200')).json();
const j=await (await fetch('/api/trackers/summary?scan_limit=0&warm=1&bg_limit=80')).json();
if(!j.ok && !j.summary) throw new Error(j.error||'Tracker summary failed');
trackerSummary=j.summary||{hashes:{},trackers:[],scanned:0,errors:[],pending:0,cached:0};
trackerSummaryStatus=(trackerSummary.trackers||[]).length?'ready':Number(trackerSummary.pending||0)?'loading':'empty';
trackerSummaryStatus=(trackerSummary.trackers||[]).length?'ready':Number(trackerSummary.pending||0)?'empty':'empty';
renderTrackerFilters();
scheduleRender(true);
if(Number(trackerSummary.pending||0)>0){
clearTimeout(trackerSummaryTimer);
trackerSummaryTimer=setTimeout(()=>refreshTrackerSummary(true).catch(()=>{}), 3500);
trackerSummaryTimer=setTimeout(()=>refreshTrackerSummary(true).catch(()=>{}), 5000);
}
}catch(e){ trackerSummaryStatus='error'; renderTrackerFilters(); console.warn('Tracker summary failed', e); }
}