Add website favicon support

This commit is contained in:
Dymas
2026-07-09 18:15:52 +02:00
parent 34c77ed9bb
commit ba408877b5
10 changed files with 35 additions and 2 deletions
+4
View File
@@ -1,5 +1,9 @@
# Changelog # Changelog
## 0.48.2 - 2026-07-09
- Added support for a checked-in `favicon.png`, serving it from the app and wiring it into the website pages so browsers show the custom favicon.
## 0.48.1 - 2026-07-09 ## 0.48.1 - 2026-07-09
- Changed queue inserts so adding a download that exactly matches an existing failed job now reuses that failed entry and resets it to pending instead of creating another duplicate failed row. - Changed queue inserts so adding a download that exactly matches an existing failed job now reuses that failed entry and resets it to pending instead of creating another duplicate failed row.
+2 -1
View File
@@ -2,7 +2,7 @@
Local web UI for a system-wide `ani-cli` install. Local web UI for a system-wide `ani-cli` install.
Current version: `0.48.1` Current version: `0.48.2`
## What it does ## What it does
@@ -15,6 +15,7 @@ Current version: `0.48.1`
- Auto-download missing episodes for `Watching` entries after later refreshes, with a per-show `Source name` override for `ani-cli` search - Auto-download missing episodes for `Watching` entries after later refreshes, with a per-show `Source name` override for `ani-cli` search
- Move fully completed libraries into Jellyfin TV or movie folders with a tracked background handoff job - Move fully completed libraries into Jellyfin TV or movie folders with a tracked background handoff job
- Send optional Discord webhook notifications - Send optional Discord webhook notifications
- Serve a checked-in `favicon.png` as the site favicon
## Pages ## Pages
+1 -1
View File
@@ -1 +1 @@
0.48.1 0.48.2
+1
View File
@@ -21,6 +21,7 @@ CONFIG_HTML = r"""<!doctype html>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>ani-cli web - config</title> <title>ani-cli web - config</title>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png">
<style> <style>
:root { :root {
color-scheme: dark; color-scheme: dark;
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

+9
View File
@@ -27,6 +27,7 @@ from app_support import (
CLIENT_DISCONNECT_ERRORS, CLIENT_DISCONNECT_ERRORS,
MAX_JSON_BODY_BYTES, MAX_JSON_BODY_BYTES,
MODE_CHOICES, MODE_CHOICES,
PROJECT_ROOT,
VERSION, VERSION,
client_address_is_local, client_address_is_local,
configured_remote_path_roots, configured_remote_path_roots,
@@ -46,6 +47,8 @@ from app_support import (
validate_discord_webhook_url, validate_discord_webhook_url,
) )
FAVICON_PATH = PROJECT_ROOT / "favicon.png"
@dataclass(frozen=True) @dataclass(frozen=True)
class HandlerContext: class HandlerContext:
add_to_watchlist: object add_to_watchlist: object
@@ -382,6 +385,7 @@ class Handler(BaseHTTPRequestHandler):
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{APP_NAME} sign in</title> <title>{APP_NAME} sign in</title>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png">
<style> <style>
:root {{ :root {{
color-scheme: dark; color-scheme: dark;
@@ -545,6 +549,11 @@ class Handler(BaseHTTPRequestHandler):
self.ensure_client_access() self.ensure_client_access()
if parsed.path == "/": if parsed.path == "/":
self.html(Handler._context(self).index_html) self.html(Handler._context(self).index_html)
elif parsed.path in {"/favicon.png", "/favicon.ico"}:
if not FAVICON_PATH.exists():
self.error(HTTPStatus.NOT_FOUND, "Favicon not found")
return
self.file(FAVICON_PATH)
elif parsed.path == "/queue": elif parsed.path == "/queue":
self.html(Handler._context(self).queue_html) self.html(Handler._context(self).queue_html)
elif parsed.path == "/config": elif parsed.path == "/config":
+1
View File
@@ -11,6 +11,7 @@ QUEUE_HTML = r"""<!doctype html>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>ani-cli web queue</title> <title>ani-cli web queue</title>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png">
<style> <style>
:root { :root {
color-scheme: dark; color-scheme: dark;
+1
View File
@@ -11,6 +11,7 @@ INDEX_HTML = r"""<!doctype html>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>ani-cli web</title> <title>ani-cli web</title>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png">
<style> <style>
:root { :root {
color-scheme: dark; color-scheme: dark;
+15
View File
@@ -176,6 +176,14 @@ class NetworkGuardTests(unittest.TestCase):
self.assertIn("Username", body) self.assertIn("Username", body)
self.assertIn("Password", body) self.assertIn("Password", body)
def test_favicon_route_serves_checked_in_png(self):
handler = DummyHandler("/favicon.png")
handler.command = "GET"
APP.Handler.do_GET(handler)
self.assertEqual(handler.file_path, http_handler.FAVICON_PATH)
def test_remote_login_page_escapes_hidden_next_value(self): def test_remote_login_page_escapes_hidden_next_value(self):
handler = DummyHandler('/?next="><script>alert(1)</script>') handler = DummyHandler('/?next="><script>alert(1)</script>')
handler.client_address = ("192.168.1.25", 8421) handler.client_address = ("192.168.1.25", 8421)
@@ -3482,6 +3490,13 @@ class TemplateHelperTests(unittest.TestCase):
self.assertIn("async function api(path, options = {})", APP.CONFIG_HTML) self.assertIn("async function api(path, options = {})", APP.CONFIG_HTML)
self.assertIn("async function api(path, options = {})", APP.WATCHLIST_HTML) self.assertIn("async function api(path, options = {})", APP.WATCHLIST_HTML)
def test_pages_include_favicon_link(self):
expected = '<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png">'
self.assertIn(expected, APP.INDEX_HTML)
self.assertIn(expected, APP.QUEUE_HTML)
self.assertIn(expected, APP.CONFIG_HTML)
self.assertIn(expected, APP.WATCHLIST_HTML)
def test_config_page_places_dependencies_before_settings_grid(self): def test_config_page_places_dependencies_before_settings_grid(self):
self.assertIn('<section class="deps" id="deps"></section>', APP.CONFIG_HTML) self.assertIn('<section class="deps" id="deps"></section>', APP.CONFIG_HTML)
self.assertIn('<div class="settings-grid">', APP.CONFIG_HTML) self.assertIn('<div class="settings-grid">', APP.CONFIG_HTML)
+1
View File
@@ -11,6 +11,7 @@ WATCHLIST_HTML = r"""<!doctype html>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>ani-cli web - watchlist</title> <title>ani-cli web - watchlist</title>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png">
<style> <style>
:root { :root {
color-scheme: dark; color-scheme: dark;