- Add new booklore API file formats - Renamed cookie for better login persistence with reverse proxy - Updated fs.py to try hardlink before atomic move from tmp dir - Fix transmission URL parsing - Fix scenario where file processing of huge files starves the healthcheck - Large enhancements to custom scripting, including passing JSON download info, more consistent activation across output types, decoupling from staging behavior, and added full documentation.
6.0 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, or upload to Booklore).
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. - Booklore output (
output.mode=booklore,phase=post_upload): the local file or folder that was uploaded (the destination is remote).
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 Booklore).
- 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 Booklore output,output.details.bookloreincludes connection details such asbase_url,library_id, andpath_id.phaseindicates when the script is running. Current values:post_transfer(folder output),post_upload(Booklore 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 + jq) (JSON payload must be enabled):
payload="$(cat)"
mode="$(echo "$payload" | jq -r '.output.mode')"
title="$(echo "$payload" | jq -r '.task.title')"
final_paths="$(echo "$payload" | jq -r '.paths.final_paths[]')"
echo "mode=$mode title=$title" >&2
echo "$final_paths" >&2
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 Booklore output, it's the local upload folder.
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.
- Booklore output mode: scripts run after upload.
$1will point at the local uploaded file (or staging folder).