Port the fish_greeting shell greeting to the new API

The original script (J621-Django/extras/fish_greeting system) downloaded
image bytes from /random/?rating=X and read the X-File-* headers. The new
API answers with JSON and a signed link, so the greeting now:

- asks /api/random/?fastfetch=1&rating=<mode> for JSON;
- parses url/j_id/filename/md5 with jq, python3, or a grep/sed fallback;
- downloads the signed link and keeps the original display path (fastfetch
  kitty/kitty-icat logos, gifsicle preprocessing for GIFs, recursion guard,
  logging with rotation, greeting_mode 0/1/2 = NSFW/SFW/Questionable);
- is configured through ~/.config/j621Greeting/config.fish or universal
  variables (J621_BASE, J621_WEB, J621_TOKEN, J621_FASTFETCH_CONFIG) instead
  of a hardcoded host, and prints the /detail/<J-ID> link on J621_WEB;
- reports the API's own 404 message when a rating has no images, and keeps
  curl quiet so failures do not spill into the greeting.

extras/fish_greeting/ contains the function, an install.fish (copies it into
the fish functions dir, creates the config once, checks dependencies) and a
README with the old-vs-new table and troubleshooting. The existing
gm-switch helper and .desktop launchers keep working (same mode file).

Tested with fish 4.9.3: syntax check on every file, guest and token runs
against the dev API (unsigned vs signed URLs), the empty-rating and
unreachable-API paths, and the grep/sed fallback with jq and python3
unavailable.
This commit is contained in:
2026-09-18 13:14:41 -05:00
parent f8667c1037
commit aee29de34a
5 changed files with 366 additions and 2 deletions
+4 -2
View File
@@ -69,8 +69,10 @@ curl -H "Authorization: Token <token>" \
- `url` is absolute, and signed for authenticated callers, so fastfetch can - `url` is absolute, and signed for authenticated callers, so fastfetch can
load it without headers. Guests get an unsigned URL and only see load it without headers. Guests get an unsigned URL and only see
guest-visible items. guest-visible items.
- `/random` and `/random/` are aliases of `/api/random/` for scripts; 404 when - `/random` and `/random/` are aliases of `/api/random/` for scripts. Behind
nothing matches the filters. the bundled nginx those aliases negotiate on `Accept`: browsers get the SPA
page, requesters like curl/wget/fastfetch get the JSON. `/api/random/` is
the unambiguous path for scripts; 404 when nothing matches the filters.
## Licence ## Licence
+74
View File
@@ -0,0 +1,74 @@
# fish_greeting (updated for the J621 rewrite)
Shows a random library image as your shell greeting, using fastfetch. This is
the updated version of the script from
`J621-Django/extras/fish_greeting system/fish_greeting.fish`, adapted to the
new REST API.
## What changed from the old script
| | Old J621-Django | This version |
| --- | --- | --- |
| Endpoint | `GET /random/?rating=X` returned **image bytes** | `GET /api/random/?fastfetch=1&rating=X` returns **JSON** |
| Image access | same response, `X-File-MD5`/`X-File-Name` headers | signed `url` from the JSON, downloaded separately |
| Unsupported types | rolled again (recursion) | server only returns png/jpg/gif in fastfetch mode |
| Config | hardcoded `https://j621.jake.i` | `J621_BASE` / `J621_TOKEN` / `J621_WEB` |
| Printed link | `/view/<md5>` on the old host | `/detail/<J-ID>` on `J621_WEB` |
| Modes, logging, gifsicle, fastfetch flags | same | same (`~/.config/fish/greeting_mode`, `~/.config/j621Logos/logs.log`) |
The `gm-switch` helper and the `.desktop` launchers from the old repo keep
working unchanged: they write the same `~/.config/fish/greeting_mode` file
(0 = NSFW, 1 = SFW, 2 = Questionable).
## Install
```fish
cd extras/fish_greeting
fish install.fish
```
Then edit `~/.config/j621Greeting/config.fish` (at least `J621_BASE`) and test:
```fish
fish_greeting
```
Requirements: `curl` (or `wget`), `fastfetch`, `file`. Optional: `gifsicle`
(GIF downscaling), `jq` or `python3` (JSON parsing — without either it falls
back to grep/sed).
## Configuration
Any of these work; the config file is read by the function on every run:
```fish
# ~/.config/j621Greeting/config.fish, or universal variables
set -g J621_BASE https://j621.rainbow-herring.ts.net # API origin
set -g J621_WEB https://j621.example.ts.net # link origin (split deploys)
set -g J621_TOKEN <api token> # signed URLs + hidden items
set -g J621_FASTFETCH_CONFIG jake # fastfetch config name
```
`J621_TOKEN` is the same token the SPA uses (`Authorization: Token …`). With
it the API signs the image URL for your account, so you also get items that
are hidden from guests. Without it the greeting runs as a guest and sees the
public library only.
## Rating filters
| `greeting_mode` | Rating requested | Label |
| --- | --- | --- |
| `0` | `e` | NSFW |
| `1` | `s` | SFW |
| `2` (default) | `q` | Questionable |
## Troubleshooting
- `No logo (No image matches those filters.)` — the library has no images for
that rating; try another mode or add files.
- `No logo (API error)` — check `J621_BASE`, and that the deployment is up
(`https://<host>/health`).
- `No logo (download failed)` — the signed URL expired (they last 24 h) or the
item was deleted between the two requests; just run it again.
- The greeting is slow — the API and image fetch have `--max-time` guards; a
slow tailnet link is usually the cause.
+17
View File
@@ -0,0 +1,17 @@
# Copy to ~/.config/j621Greeting/config.fish and adjust. All of these can also
# be universal variables, e.g. `set -Ux J621_BASE https://j621.example.ts.net`.
# API origin (no trailing slash needed).
set -g J621_BASE https://j621.rainbow-herring.ts.net
# Web origin used in the printed link. Only needed when the SPA is served
# from a different host than the API (split deployment).
# set -g J621_WEB https://j621-frontend.rainbow-herring.ts.net
# API token. Optional: gives you signed image URLs and access to items that
# are hidden from guests. Create one with `manage.py drf_create_token <user>`
# or copy it from the app (it is the same token the SPA stores).
# set -g J621_TOKEN paste-your-token-here
# fastfetch config name used for the greeting logo.
# set -g J621_FASTFETCH_CONFIG jake
+219
View File
@@ -0,0 +1,219 @@
# fish_greeting — show a random library image as the shell greeting.
#
# Updated for the J621 Docker/REST rewrite: the API answers with JSON that
# carries a signed URL instead of streaming the image, so this function asks
# for one random png/jpg/gif (fastfetch mode), downloads the signed link and
# hands the file to fastfetch. Based on the original script from
# J621-Django/extras/fish_greeting system/fish_greeting.fish.
#
# Requires: curl (or wget) and fastfetch; gifsicle is used for animated GIFs
# (without it, the GIF is shown as-is).
#
# Rating modes (same file as the original script):
# ~/.config/fish/greeting_mode 0 = NSFW (explicit), 1 = SFW (safe),
# 2 = Questionable (default)
#
# Configuration, any of these (all optional):
# ~/.config/j621Greeting/config.fish with `set -g VAR value` lines, or
# universal variables, e.g. `set -Ux J621_BASE https://j621.example.ts.net`
# J621_BASE API origin (default https://j621.rainbow-herring.ts.net)
# J621_TOKEN API token, sent as `Authorization: Token …`; needed for
# signed URLs and for hidden-from-guests items (optional)
# J621_WEB Web origin used in the printed link (defaults to J621_BASE)
# J621_FASTFETCH_CONFIG fastfetch config name (default "jake")
function __j621_json_field --argument-names json field
if command -v jq >/dev/null 2>&1
printf '%s' "$json" | jq -r --arg field "$field" '.[$field] // empty'
else if command -v python3 >/dev/null 2>&1
printf '%s' "$json" | python3 -c "import json,sys; value = json.load(sys.stdin).get(sys.argv[1], ''); print(value if value is not None else '')" $field
else
printf '%s' "$json" | grep -o "\"$field\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed -E 's/.*:[[:space:]]*"//; s/"$//'
end
end
function __j621_fetch_random --argument-names rating
set base (string trim --right --chars=/ "$J621_BASE")
set api_url "$base/api/random/?fastfetch=1&rating=$rating"
set curl_args -s -L --max-time 20
if set -q J621_TOKEN; and test -n "$J621_TOKEN"
set -a curl_args -H "Authorization: Token $J621_TOKEN"
end
if command -v curl >/dev/null 2>&1
curl $curl_args "$api_url" | string collect
else if command -v wget >/dev/null 2>&1
wget -qO- "$api_url" | string collect
end
end
function fish_greeting --argument-names depth
# Initialize depth to 0 if not provided or empty
if not set -q depth; or test -z "$depth"
set depth 0
end
set MAX_DEPTH 3
# --- Configuration ---
set config_file ~/.config/j621Greeting/config.fish
if test -f $config_file
source $config_file
end
set -q J621_BASE; or set -g J621_BASE https://j621.rainbow-herring.ts.net
set -q J621_WEB; or set -g J621_WEB $J621_BASE
set -q J621_FASTFETCH_CONFIG; or set -g J621_FASTFETCH_CONFIG jake
set web_base (string trim --right --chars=/ "$J621_WEB")
# --- Setup logging ---
set log_dir ~/.config/j621Logos
if not test -d $log_dir
mkdir -p $log_dir
end
set log_file $log_dir/logs.log
# --- Mode selection ---
if test -f ~/.config/fish/greeting_mode
set mode (cat ~/.config/fish/greeting_mode | string trim)
else
set mode 2
end
switch $mode
case "0"
set rating e
set modename "NSFW"
case "1"
set rating s
set modename "SFW"
case "2"
set rating q
set modename "Questionable"
case "*"
echo "Unknown mode, using default NSFW"
set rating e
set modename "NSFW"
end
# --- Ask the API for a random image (JSON with a signed URL) ---
set json (__j621_fetch_random $rating)
set image_url (__j621_json_field "$json" url)
set j_id (__j621_json_field "$json" j_id)
set md5 (__j621_json_field "$json" md5)
set filename (__j621_json_field "$json" filename)
if test -z "$image_url"
set detail (__j621_json_field "$json" detail)
fastfetch --config "$J621_FASTFETCH_CONFIG"
if test -n "$detail"
printf '\e[4;2;38;2;138;0;222;49mNo logo (%s)\e[0m\n' "$detail"
else
printf '\e[4;2;38;2;138;0;222;49mNo logo (API error)\e[0m\n'
end
echo "$(date '+%Y-%m-%d %H:%M:%S') No logo for rating=$rating: $detail" >> "$log_file"
return
end
# --- Download the signed image ---
set tempfile (mktemp /tmp/fastfetch_logo_XXXXXX)
set download_success 0
if command -v curl >/dev/null 2>&1
curl -s -L --max-time 60 -o "$tempfile" "$image_url"
if test $status -eq 0 -a -s "$tempfile"
set download_success 1
end
else if command -v wget >/dev/null 2>&1
wget -q -O "$tempfile" "$image_url"
if test $status -eq 0 -a -s "$tempfile"
set download_success 1
end
end
if test $download_success -ne 1
fastfetch --config "$J621_FASTFETCH_CONFIG"
printf '\e[4;2;38;2;138;0;222;49mNo logo (download failed)\e[0m\n'
echo "$(date '+%Y-%m-%d %H:%M:%S') Download failed for $j_id" >> "$log_file"
rm -f "$tempfile"
return
end
# --- Determine MIME type ---
set mime (file --mime-type -b "$tempfile" 2>/dev/null | string trim | string collect)
# --- Accept only PNG, JPEG, GIF; everything else causes one more roll ---
if not string match -q "image/png" "$mime"; and not string match -q "image/jpeg" "$mime"; and not string match -q "image/gif" "$mime"
if test $depth -lt $MAX_DEPTH
fish_greeting (math $depth + 1)
else
printf '\e[31mMax recursion depth reached, skipping unsupported file type: %s\e[0m\n' "$mime"
end
if test $depth -eq 0
printf '\e[31mForked (Got unsupported type: %s - %s)\e[0m\n' "$mime" "$filename"
if test -n "$md5"
echo "Link: $web_base/view/$md5"
end
end
rm -f "$tempfile" "$tempfile.tmp" 2>/dev/null
return
end
# --- Process the file (only PNG, JPEG, GIF reach here) ---
set logo_type "kitty"
set processing_success 1
switch "$mime"
case "image/png" "image/jpeg"
set logo_type "kitty"
case "image/gif"
set processed_ok 0
set simple_file (mktemp /tmp/fastfetch_simple_XXXXXX)
set gif_err (mktemp)
if command -v gifsicle >/dev/null 2>&1
gifsicle --colors 255 "$tempfile" > "$simple_file" 2> "$gif_err"
set gif_exit $status
if test $gif_exit -eq 0 -a -s "$simple_file"
gifsicle --unoptimize --resize-fit 360x360 "$simple_file" > "$tempfile.tmp" 2> "$gif_err"
set final_exit $status
set gif_warnings (cat "$gif_err" | string trim)
if test $final_exit -eq 0 -a -s "$tempfile.tmp" -a -z "$gif_warnings"
mv "$tempfile.tmp" "$tempfile"
set processed_ok 1
set logo_type "kitty-icat"
echo "$(date '+%Y-%m-%d %H:%M:%S') GIF processed with gifsicle" >> "$log_file"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') gifsicle final failed: $gif_warnings" >> "$log_file"
end
else
echo "$(date '+%Y-%m-%d %H:%M:%S') gifsicle --colors failed" >> "$log_file"
end
end
rm -f "$simple_file" "$gif_err"
if test $processed_ok -eq 0
set logo_type "kitty-icat"
echo "$(date '+%Y-%m-%d %H:%M:%S') Using original GIF (processing failed)" >> "$log_file"
end
end
# --- Display result ---
if test $processing_success -eq 1
fastfetch --config "$J621_FASTFETCH_CONFIG" --logo-type "$logo_type" --logo "$tempfile" --logo-width 35
set timestamp (date '+%Y-%m-%d %H:%M:%S')
echo "$timestamp Downloaded: $j_id $filename [$modename]" >> "$log_file"
if test -n "$filename"
printf '\e[4;2;38;2;138;0;222;49mLogo from API: %s (%s, %s)\e[0m\n' "$filename" "$modename" "$j_id"
echo "Link: $web_base/detail/$j_id"
else
printf '\e[4;2;38;2;138;0;222;49mLogo from API (%s)\e[0m\n' "$modename"
end
else
fastfetch --config "$J621_FASTFETCH_CONFIG"
printf '\e[4;2;38;2;138;0;222;49mNo logo (Image processing error)\e[0m\n'
end
# --- Keep only the last 1000 lines of the log ---
set log_tmp (mktemp)
tail -n 1000 "$log_file" > "$log_tmp" 2>/dev/null
mv "$log_tmp" "$log_file"
# --- Cleanup ---
rm -f "$tempfile" "$tempfile.tmp" 2>/dev/null
end
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env fish
# Install the J621 fish greeting:
# * copies fish_greeting.fish into ~/.config/fish/functions/
# * creates ~/.config/j621Greeting/config.fish from the example (once)
# * checks the tools it needs
#
# Usage: fish install.fish
set -l here (path resolve (dirname (status --current-filename)))
set -l functions_dir ~/.config/fish/functions
set -l config_dir ~/.config/j621Greeting
set -l problems 0
mkdir -p $functions_dir
cp "$here/fish_greeting.fish" "$functions_dir/fish_greeting.fish"
echo "Installed $functions_dir/fish_greeting.fish"
mkdir -p $config_dir
if test -f "$config_dir/config.fish"
echo "Keeping existing $config_dir/config.fish"
else
cp "$here/config.example.fish" "$config_dir/config.fish"
echo "Created $config_dir/config.fish — edit J621_BASE (and J621_TOKEN) there"
end
for tool in curl fastfetch file
if not command -v $tool >/dev/null 2>&1
set problems 1
echo "Missing required tool: $tool"
end
end
if not command -v curl >/dev/null 2>&1; and not command -v wget >/dev/null 2>&1
set problems 1
echo "Missing required tool: curl (or wget)"
end
for tool in gifsicle jq python3
if not command -v $tool >/dev/null 2>&1
echo "Optional tool not found: $tool (the greeting still works)"
end
end
if test $problems -eq 0
echo
echo "Done. Test it now with: fish_greeting"
echo "Rating mode lives in ~/.config/fish/greeting_mode (0=NSFW, 1=SFW, 2=Questionable)."
echo "The old gm-switch tool / .desktop launchers keep working — same file."
else
echo
echo "Install the missing tools, then run: fish_greeting"
end