These are the leftovers from the docs check, the fixes that did not belong in the other three. Claude did the comparison and wrote the changes here too, so I would still like a human look before it merges. The one new piece of prose worth a read is `troubleshooting.md`. It was a TODO page that two other pages link to, so I filled it with only what I could confirm in the code: where the logs go, `LOG_LEVEL`, the `DEBUG=true` bundle from the bug report template, and reporting an issue. Happy to drop it back to a stub if you would rather write that one yourself. - `configuration.md`, `custom-scripts.md`, `users-and-requests.md`: Booklore is Grimmory in the UI now. The stored mode value is still `booklore`, and the custom script doc says so. The hardlink heading now matches the "Hardlink Book Torrents" and "Hardlink Audiobook Torrents" checkboxes. - `custom-scripts.md`: the email output runs scripts too (`phase=post_email`, `output.mode=email`, details under `output.details.email`). It was missing from the phase list. - `users-and-requests.md`: a delivery that fails with no retry available goes back to pending on its own, and the admin reopen action is for rejected requests. The page said an admin reopens a failed delivery. Also noted that rules cannot use Request Book. - `hardcover-sync.md`: added the missing `AUTO_DOWNLOAD_AUDIOBOOK_MIN_SIZE_PERCENT` row and a sentence in the ranking paragraph. - `api-access.md`: an admin session also reads `/api/stats`, and what the 403 says. - `installation.md`: points at the Lite, Tor and WireGuard variants. Docs only, no code touched.
6.7 KiB
Custom Scripts
Shelfmark can run an executable you provide after a download task completes successfully. The script runs after the selected output has finished (for example: transfer to the folder destination, upload to Grimmory, or send by email).
Quick Start (Recommended)
- Put your script on the machine that runs Shelfmark.
- Make it executable.
- Set it in Shelfmark (Settings -> Advanced -> Custom Script Path).
Example:
chmod +x /path/to/your/scripts/post_process.sh
Docker Users
If you run Shelfmark in Docker, the script must exist inside the container. The easiest way is to mount a folder of scripts, then point Shelfmark at the container path in the UI.
services:
shelfmark:
image: ghcr.io/calibrain/shelfmark:latest
volumes:
- /path/to/your/scripts:/scripts:ro
Then set:
- Settings -> Advanced -> Custom Script Path:
/scripts/post_process.sh
Docker Compose: Configure Via Environment Variables (Optional)
services:
shelfmark:
environment:
- CUSTOM_SCRIPT=/scripts/post_process.sh
- CUSTOM_SCRIPT_PATH_MODE=absolute
- CUSTOM_SCRIPT_JSON_PAYLOAD=true
Script Behaviour
When enabled, Shelfmark runs your script once per successful task:
<custom_script_path> "<target_path>"
$1is always set to the target path.- If Custom Script JSON Payload is enabled, Shelfmark writes a JSON document to stdin (UTF-8).
- If JSON payload is disabled, stdin is empty (EOF).
- Timeout: 300 seconds (5 minutes)
- Exit code:
0= success; anything else = the task is marked as Error - Concurrency: downloads can run in parallel, so your script may be invoked concurrently for different tasks.
- Runtime: the script runs inside the Shelfmark container (if you use Docker) under the same user as Shelfmark.
The Target Path ($1)
Shelfmark chooses a "best single path" for the task:
- If the output produced exactly one local file: that file path.
- If the output produced multiple local files: a directory path (the common parent directory of those files).
What the target path refers to depends on the output mode:
- Folder output (
output.mode=folder,phase=post_transfer): the final imported file or folder inside your destination. - Grimmory output (
output.mode=booklore,phase=post_upload): the local file or folder that was uploaded (the destination is remote). - Email output (
output.mode=email,phase=post_email): the file that was sent, or the folder holding the files when there were several.
By default, $1 is an absolute path inside the Shelfmark container (or on your host, if you are not using Docker).
JSON Payload (stdin)
Configure in: Settings -> Advanced -> Custom Script JSON Payload
When enabled, Shelfmark sends a versioned JSON payload to your script via stdin (and still passes $1). This is the recommended way to write robust scripts, especially for multi-file imports (audiobooks) and output-specific context (like Grimmory).
- The JSON payload always includes absolute paths in
paths.*, even if you set Custom Script Path Mode torelativefor$1. output.modetells you which output ran.output.detailsis output-specific. For Grimmory output (the mode value is stillbooklore),output.details.bookloreincludes connection details such asbase_url,library_id, andpath_id. For email output,output.details.emailhasto,label,host,port, andsecurity.phaseindicates when the script is running. Current values:post_transfer(folder output),post_upload(Grimmory output),post_email(email output).transferis only included for outputs that do a local transfer (for example the folder output).
If JSON payload is disabled, stdin is empty (EOF). Don't cat stdin unless you've enabled the payload.
Example payload shape:
{
"version": 1,
"phase": "post_transfer",
"task": {
"task_id": "abc123",
"source": "direct",
"title": "Foundation",
"author": "Isaac Asimov"
},
"output": {
"mode": "folder",
"organization_mode": "organize"
},
"paths": {
"destination": "/data/library/books",
"target": "/data/library/books/Isaac Asimov/Foundation/Foundation.epub",
"final_paths": [
"/data/library/books/Isaac Asimov/Foundation/Foundation.epub"
]
},
"transfer": {
"op_counts": {"copy": 1, "move": 0, "hardlink": 0},
"use_hardlink": false,
"is_torrent": false,
"preserve_source": false
}
}
Example (bash + python3) (JSON payload must be enabled):
payload="$(cat)"
target="$1"
PAYLOAD="$payload" TARGET="$target" python3 - <<'PY'
import json
import os
import sys
payload = json.loads(os.environ["PAYLOAD"])
print(f"target={os.environ['TARGET']}", file=sys.stderr)
print(
f"mode={payload['output']['mode']} title={payload['task']['title']}",
file=sys.stderr,
)
for path in payload["paths"]["final_paths"]:
print(path, file=sys.stderr)
PY
Example (Python) (works whether JSON payload is enabled or not):
#!/usr/bin/env python3
import json
import sys
target = sys.argv[1]
raw = sys.stdin.read()
payload = json.loads(raw) if raw.strip() else None
print(f"target={target}", file=sys.stderr)
if payload:
print(f"mode={payload['output']['mode']} phase={payload['phase']}", file=sys.stderr)
Advanced Options
Absolute vs Relative Target Paths
Configure in: Settings -> Advanced -> Custom Script Path Mode
This setting controls what gets passed as $1:
absolute(default): pass an absolute path.relative: pass a path relative to the output's "destination root", and run the script with$PWDset to that root.
For folder output, the destination root is your configured destination folder. For Grimmory output, it's the local upload folder. For email output, it's the folder holding the files that were sent.
Example (folder destination is /data/library/books, and the imported file ended up in Isaac Asimov/Foundation/Foundation.epub):
# Absolute mode:
$PWD is unchanged
$1 = /data/library/books/Isaac Asimov/Foundation/Foundation.epub
# Relative mode:
$PWD = /data/library/books
$1 = Isaac Asimov/Foundation/Foundation.epub
Note: if the target is the destination folder itself, relative mode may pass ..
Notes And Caveats
- Hardlinks and torrents: if you use hardlinking to keep seeding, avoid scripts that modify file contents, since hardlinked files share data with the seeding copy.
- Grimmory output mode: scripts run after upload.
$1will point at the local uploaded file (or staging folder). - Email output mode: scripts run after the message is sent.
$1will point at the file that was sent (or the folder holding the files).