From 02875fdc92a624dfe0b93c6cfa158c0c2f63517f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 8 May 2026 20:09:30 +0200 Subject: [PATCH] favicons --- pytorrent/cli.py | 26 ++++++++++++++++++++++++++ pytorrent/routes/api.py | 22 ++++++++++++++++++++-- pytorrent/services/tracker_cache.py | 5 +++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/pytorrent/cli.py b/pytorrent/cli.py index 8038c96..edd950f 100644 --- a/pytorrent/cli.py +++ b/pytorrent/cli.py @@ -6,6 +6,7 @@ import sys from .db import connect, init_db, utcnow from .services.auth import password_hash +from .services import tracker_cache def reset_password(username: str, password: str) -> bool: @@ -30,6 +31,20 @@ def reset_password(username: str, password: str) -> bool: return True + +def fetch_tracker_favicon(domain: str, refresh: bool = True) -> str: + """Note: Download or refresh one tracker favicon from CLI without starting the web server.""" + clean = tracker_cache.tracker_domain(domain) + if not clean: + raise ValueError("Tracker domain is required") + init_db() + path, mime = tracker_cache.favicon_path(clean, enabled=True, force=refresh) + if not path: + row = tracker_cache.favicon_cache_row(clean) + detail = (row or {}).get("error") if row else "favicon not found" + raise RuntimeError(str(detail or "favicon not found")) + return f"{path} ({mime or 'unknown'})" + def _password_from_args(args: argparse.Namespace) -> str: """Note: Allow the password to be passed as an argument or entered securely in interactive mode.""" if args.password is not None: @@ -51,6 +66,11 @@ def build_parser() -> argparse.ArgumentParser: reset.add_argument("password", nargs="?", help="New password; omit to type it interactively") reset.set_defaults(func=_cmd_reset_password) + icon = sub.add_parser("tracker-favicon", help="Download or refresh a tracker favicon cache file") + icon.add_argument("domain", help="Tracker domain, e.g. t.pte.nu") + icon.add_argument("--no-refresh", action="store_true", help="Use fresh cache when available") + icon.set_defaults(func=_cmd_tracker_favicon) + return parser @@ -64,6 +84,12 @@ def _cmd_reset_password(args: argparse.Namespace) -> int: return 1 +def _cmd_tracker_favicon(args: argparse.Namespace) -> int: + """Note: Run favicon discovery from CLI and print the saved file path.""" + print(fetch_tracker_favicon(args.domain, refresh=not args.no_refresh)) + return 0 + + def main(argv: list[str] | None = None) -> int: """Note: Main CLI entrypoint with error handling and without starting the web app.""" parser = build_parser() diff --git a/pytorrent/routes/api.py b/pytorrent/routes/api.py index 51937b9..5795a14 100644 --- a/pytorrent/routes/api.py +++ b/pytorrent/routes/api.py @@ -508,15 +508,33 @@ def trackers_summary(): @bp.get("/trackers/favicon/") +@bp.get("/tracker-favicon/") def tracker_favicon(domain: str): prefs = preferences.get_preferences() - enabled = bool(prefs and prefs.get("tracker_favicons_enabled")) force = str(request.args.get("refresh") or "").lower() in {"1", "true", "yes", "force"} + # Note: Manual refresh must work from CLI even when tracker favicons are disabled in Preferences. + enabled = force or bool(prefs and prefs.get("tracker_favicons_enabled")) static_url = tracker_cache.favicon_public_url(domain, enabled=enabled, create=True, force=force) if static_url: # Note: The API only discovers/cache-warms the icon; the browser receives the file from /static/tracker_favicons/. return redirect(static_url, code=302) - abort(404) + cached = tracker_cache.favicon_cache_row(domain) + return jsonify({ + "ok": False, + "error": "favicon not found", + "domain": tracker_cache.tracker_domain(domain), + "enabled": bool(enabled), + "cached_error": (cached or {}).get("error") if cached else None, + }), 404 + + +@bp.get("/trackers/favicon") +def tracker_favicon_query(): + # Note: Query-string alias makes cache warming easier from shell scripts where path routing/proxies may differ. + domain = str(request.args.get("domain") or "").strip() + if not domain: + return jsonify({"ok": False, "error": "domain is required"}), 400 + return tracker_favicon(domain) @bp.get("/torrent-stats") def torrent_stats_get(): diff --git a/pytorrent/services/tracker_cache.py b/pytorrent/services/tracker_cache.py index 304396f..2ab0230 100644 --- a/pytorrent/services/tracker_cache.py +++ b/pytorrent/services/tracker_cache.py @@ -300,6 +300,11 @@ def _cached_favicon(domain: str): return conn.execute("SELECT * FROM tracker_favicon_cache WHERE domain=?", (clean,)).fetchone() +def favicon_cache_row(domain: str): + """Note: Expose the favicon cache row for diagnostics without duplicating SQL in routes or CLI.""" + return _cached_favicon(domain) + + def favicon_path(domain: str, enabled: bool = True, force: bool = False) -> tuple[Path | None, str | None]: clean = tracker_domain(domain) if not enabled or not clean: