Add a Random image endpoint and SPA page (with fastfetch mode)

Backend: GET /api/random/ (aliases /random and /random/) returns a random
library image with:
- rating=s,q,e filtering (comma separated, default any);
- fastfetch mode (?fastfetch=1 or any User-Agent containing "fastfetch")
  that only considers png/jpg/gif - what terminal viewers can show;
- JSON with j_id, filename, extension, rating, size, e621 id plus absolute
  url/download_url/thumbnail_url. Authenticated callers get signed URLs so
  fastfetch and image viewers can load them without headers; guests get
  unsigned URLs and never receive hidden_from_guests items.

Tests: apps/library/tests/test_random.py (8 tests) covering the response
contract, guest signatures, image-only default, the fastfetch format
restriction (flag and User-Agent), rating filters, guest visibility and the
short alias.

Frontend: /random page with rating pills, R to roll, Open/Download and a
library link, plus navigation and command palette entries; needs a backend,
hidden in local mode.

nginx: /random negotiates on Accept so browsers keep getting the SPA while
scripts get the JSON (verified with the proxy and frontend containers).

Also fixes a regression from the SSRF change: the guest download proxy
still referenced the removed 'parsed' variable on its success path, so
every proxied download would have 500'd. Redirect hops are now covered by
tests with a mocked requests.get.
This commit is contained in:
2026-09-18 13:06:36 -05:00
parent f25526782c
commit f8667c1037
14 changed files with 573 additions and 5 deletions
+34
View File
@@ -17,6 +17,13 @@ map $http_x_forwarded_proto $j621_forwarded_proto {
"" $scheme;
}
# /random is both the SPA route and the shell-greeting shortcut: browsers
# (Accept: text/html) get the SPA, scripts (curl/wget/fetch) get the API JSON.
map $http_accept $j621_random_target {
default @j621_random_api;
~*text/html @j621_random_spa;
}
server {
listen 80;
server_name _;
@@ -26,6 +33,33 @@ server {
return 200 "ok\n";
}
location ~ ^/random/?$ {
error_page 418 = $j621_random_target;
return 418;
}
location @j621_random_api {
set $j621_backend http://backend:8000;
proxy_pass $j621_backend$request_uri;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $j621_forwarded_proto;
}
location @j621_random_spa {
set $j621_frontend http://frontend:80;
proxy_pass $j621_frontend$request_uri;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $j621_forwarded_proto;
}
location ~ ^/(api|admin|static|health)(/|$) {
set $j621_backend http://backend:8000;
proxy_pass $j621_backend$request_uri;