6.2 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 + 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 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).