favicons
This commit is contained in:
@@ -491,12 +491,17 @@ def trackers_summary():
|
|||||||
if not profile:
|
if not profile:
|
||||||
return ok({"summary": {"hashes": {}, "trackers": [], "errors": [], "scanned": 0, "pending": 0}, "error": "No profile"})
|
return ok({"summary": {"hashes": {}, "trackers": [], "errors": [], "scanned": 0, "pending": 0}, "error": "No profile"})
|
||||||
try:
|
try:
|
||||||
# Note: Tracker summary uses the local torrent snapshot and refreshes only a small cache batch per request.
|
# 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 80)))
|
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")]
|
hashes = [t.get("hash") for t in torrent_cache.snapshot(profile["id"]) if t.get("hash")]
|
||||||
prefs = preferences.get_preferences()
|
prefs = preferences.get_preferences()
|
||||||
include_favicons = bool(prefs and prefs.get("tracker_favicons_enabled"))
|
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})
|
return ok({"summary": summary})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return ok({"summary": {"hashes": {}, "trackers": [], "errors": [{"error": str(exc)}], "scanned": 0, "pending": 0}, "error": str(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 mimetypes
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
import threading
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -18,6 +19,8 @@ FAVICON_CACHE_TTL_SECONDS = 7 * 24 * 60 * 60
|
|||||||
TRACKER_SCAN_LIMIT = 80
|
TRACKER_SCAN_LIMIT = 80
|
||||||
FAVICON_DIR = BASE_DIR / "data" / "tracker_favicons"
|
FAVICON_DIR = BASE_DIR / "data" / "tracker_favicons"
|
||||||
PUBLIC_FAVICON_BASE = "/static/tracker_favicons"
|
PUBLIC_FAVICON_BASE = "/static/tracker_favicons"
|
||||||
|
_TRACKER_SCAN_LOCKS: dict[int, threading.Lock] = {}
|
||||||
|
_TRACKER_SCAN_LOCKS_GUARD = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
class _IconParser(HTMLParser):
|
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:
|
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."""
|
"""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.
|
# 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>`;
|
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(){
|
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(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>';
|
if(hasTorrentSnapshot && torrents.size) return '<div class="tracker-filter-empty">No trackers found</div>';
|
||||||
return '<div class="tracker-filter-empty">Waiting for torrents...</div>';
|
return '<div class="tracker-filter-empty">Waiting for torrents...</div>';
|
||||||
}
|
}
|
||||||
@@ -273,15 +273,15 @@
|
|||||||
renderTrackerFilters();
|
renderTrackerFilters();
|
||||||
try{
|
try{
|
||||||
// Note: Nie wysyłamy 13k hashy w URL; backend bierze lokalny snapshot i doczytuje cache małymi porcjami.
|
// 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');
|
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};
|
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();
|
renderTrackerFilters();
|
||||||
scheduleRender(true);
|
scheduleRender(true);
|
||||||
if(Number(trackerSummary.pending||0)>0){
|
if(Number(trackerSummary.pending||0)>0){
|
||||||
clearTimeout(trackerSummaryTimer);
|
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); }
|
}catch(e){ trackerSummaryStatus='error'; renderTrackerFilters(); console.warn('Tracker summary failed', e); }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user