Bun-based backend implemented in the podible codebase.
The service provides:
- SQLite-backed library, releases, assets, jobs, and settings.
- Torznab search.
- rTorrent snatch/download polling.
- Import pipeline with hardlinking into the configured library root.
- Open Library search + identifier-based library creation.
- Audio streaming, chapters, RSS/JSON feeds.
- Ebook direct download endpoint.
- Mock Torznab/rTorrent and end-to-end tests.
- Bun
1.3+ ffprobe(used for audio duration/chapters)ffmpeg(used by existing podible media behaviors)
bun installbun run server.tsRuntime bootstrap uses one directory:
CONFIG_DIR(default${TMPDIR:-/tmp}/podible-config) forpodible.sqlite
libraryRoot is stored in application settings (settings.get / settings.update).
Open Library covers are stored alongside each imported book inside libraryRoot.
Settings default to API key auth (Authorization: Bearer <key> or X-API-Key).
On boot, the server logs the active API key from settings.
For localhost development, set settings auth.mode to local via settings.update RPC.
POST /rpc(control/data APIs via JSON-RPC 2.0)GET /rpc/{namespace}/{method}(read-only convenience bridge, query params -> RPC params)GET /assets?bookId=GET /stream/{assetId}.{ext}GET /chapters/{assetId}.jsonGET /covers/{bookId}.jpgGET /feed.xmlGET /feed.jsonGET /ebook/{assetId}
Removed REST control routes now return 404:
/health,/server/settings/openlibrary/search/library,/library/{bookId},/library/refresh/search,/snatch/releases,/downloads,/downloads/{id},/downloads/{id}/retry/import/reconcile
system.healthsystem.serversettings.getsettings.updateopenlibrary.searchlibrary.listlibrary.getlibrary.createlibrary.deletelibrary.refreshlibrary.acquirelibrary.reportImportIssuelibrary.rehydratesearch.runagent.search.plansnatch.createreleases.listdownloads.listdownloads.getdownloads.retryjobs.listjobs.getagent.import.planimport.reconcileimport.inspectimport.manual
RPC request shape:
{
"jsonrpc": "2.0",
"id": 1,
"method": "settings.get",
"params": {}
}Read-only bridge examples:
curl "http://localhost/rpc/system/health"
curl "http://localhost/rpc/library/list?limit=20&q=dune"
curl "http://localhost/rpc/library/get?bookId=1"Bridge constraints:
- Read-only methods only (
settings.update,library.create,snatch.create, etc. are blocked). - Responses still use JSON-RPC envelopes with
id: null. - Canonical control/data write path remains
POST /rpc.
settings.get / settings.update use:
{
"torznab": [
{
"name": "prowlarr",
"baseUrl": "http://localhost:9696",
"apiKey": "...",
"categories": { "audio": "audio", "ebook": "book" }
}
],
"rtorrent": {
"transport": "http-xmlrpc",
"url": "http://127.0.0.1/RPC2",
"username": "",
"password": "",
"downloadPath": ""
},
"libraryRoot": "/media/library",
"polling": { "rtorrentMs": 5000, "scanMs": 30000 },
"recovery": { "stalledTorrentMinutes": 10 },
"feed": { "title": "Books", "author": "Unknown" },
"auth": { "mode": "apikey", "key": "..." },
"agents": {
"enabled": false,
"provider": "openai-responses",
"model": "gpt-5-mini",
"apiKey": "",
"lowConfidenceThreshold": 0.45,
"timeoutMs": 30000
},
"notifications": {
"pushover": {
"enabled": false,
"apiToken": "",
"userKey": ""
}
}
}Agent behavior:
- Deterministic ranking/selection remains the default behavior.
- Responses API is used only when
agents.enabled=trueand a trigger condition is met (forceAgent, prior failure, or low confidence). - Missing/failed agent calls fall back to deterministic selection.
Download recovery behavior:
- Download jobs continuously watch rTorrent state; this is the stalled-torrent watcher.
- If rTorrent reports any error on an incomplete torrent, Podible cancels that download job and queues a forced agent reacquire for the same media while rejecting the failed torrent URL/guid/infohash.
- If the forced reacquire job later exhausts retries or produces no usable candidate, Podible sends a notification.
recovery.stalledTorrentMinutescontrols how long an incomplete torrent can sit with no progress before Podible treats it as stalled and auto-reacquires.- Pushover delivery is best-effort and requires
notifications.pushover.enabled=trueplusapiTokenanduserKey.
Search across Open Library:
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"openlibrary.search","params":{"q":"Hyperion Dan Simmons","limit":10}}'Add by Open Library work key:
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"library.create","params":{"openLibraryKey":"/works/OL45804W"}}'Re-trigger auto-acquire for an existing book:
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":21,"method":"library.acquire","params":{"bookId":123}}'Target only one media type:
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":22,"method":"library.acquire","params":{"bookId":123,"media":["ebook"]}}'Force agent-powered reacquire (user-triggered recovery):
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":23,"method":"library.acquire","params":{"bookId":123,"media":["audio"],"forceAgent":true,"priorFailure":true}}'Report an imported file as wrong (attempt forced agent re-import first, then queue forced agent reacquire if needed):
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":24,"method":"library.reportImportIssue","params":{"bookId":123,"mediaType":"audio"}}'Rehydrate metadata for existing books (all or one):
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"library.rehydrate","params":{}}'curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":4,"method":"library.rehydrate","params":{"bookId":123}}'Inspect background jobs and acquire outcomes:
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":5,"method":"jobs.list","params":{"limit":20}}'curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":6,"method":"jobs.get","params":{"jobId":42}}'Inspect a local download path before manual import:
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":7,"method":"import.inspect","params":{"path":"/data/downloads/box-set"}}'Manual import with explicit file selection (useful for box sets):
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":8,"method":"import.manual","params":{"bookId":123,"mediaType":"audio","path":"/data/downloads/box-set","selectedPaths":["/data/downloads/box-set/Disc 1/01.mp3","/data/downloads/box-set/Disc 1/02.mp3"]}}'Plan search candidate selection (no side effects):
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":9,"method":"agent.search.plan","params":{"query":"Dune Frank Herbert","media":"audio","bookId":123}}'Plan manual import file selection (no side effects):
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":10,"method":"agent.import.plan","params":{"path":"/data/downloads/box-set","mediaType":"audio","bookId":123}}'Run all tests:
bun testTypecheck:
bun run typecheckCurrent suites include:
- unit tests (schema, repo, status, auth, torznab, rtorrent, media)
- integration HTTP tests
- end-to-end flow tests with mocks in
test/mocks
- Idempotency is enforced by globally unique
releases.info_hash. - Job worker uses queue claim/requeue semantics with retry backoff.
- Job type split:
acquireis targeted auto-search/snatch for one book, whilefull_library_refreshscans and imports existing filesystem content. - Scanner and
library.rehydratehydrate missing metadata from Open Library (work id/language/publish date/description/cover where available). - Import strategy uses hardlinks only; cross-device
EXDEVis surfaced as an error. - Snatch requires
.torrentURLs (magnet links are out of scope). - Snatch computes canonical infohash from downloaded
.torrentbytes; Torznabinfohashattrs are optional. - JSON-RPC batch requests are intentionally unsupported in v1.
- Playback position APIs are intentionally out of scope for this phase.