favicons
This commit is contained in:
@@ -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)})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user