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
On a new install, choose one absolute library root in Settings > Storage
(storageRoot in the API). Podible derives the books, movies and TV directories
below it. Mount the same directory at the same absolute path in Podible and
Plex, and point each Plex section at its displayed symlink view, never its
canonical objects directory. Existing installs keep their saved paths;
changing the root after adding media requires a separate storage migration.
Settings default to Plex browser sign-in.
auth.mode = "plex"enables the normal browser login flow.- Browser routes use Podible session cookies.
- App clients can use the app-login flow:
auth.beginAppLogin-> browser sign-in ->auth.exchangefor a bearer token. - JSON-RPC methods are now scoped by auth level (
public,user,admin).
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.serverauth.beginAppLoginauth.exchangeauth.meauth.logoutsettings.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. - Browser login bootstrap still uses normal HTTP redirect routes behind the scenes.
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": ""
},
"storageRoot": "/media/podible",
"libraryRoot": "/media/podible/books",
"polling": { "rtorrentMs": 5000 },
"recovery": { "stalledTorrentMinutes": 10 },
"feed": { "title": "Books", "author": "Unknown" },
"auth": {
"mode": "plex",
"appRedirectURIs": ["kindling://auth/podible"],
"plex": {
"productName": "Podible",
"ownerToken": "",
"machineId": ""
}
},
"agents": {
"apiKey": "",
"editionPreference": "",
"timeoutMs": 30000
},
"notifications": {
"delivery": "dry-run",
"pushover": {
"enabled": false,
"apiToken": "",
"userKey": ""
}
}
}Agent behavior:
- Deterministic ranking/selection remains the default behavior.
- Responses API is used only when
agents.apiKeyis configured and 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"}}'List alternate Open Library covers for an existing book:
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"openlibrary.covers","params":{"bookId":123,"limit":50}}'Apply a selected Open Library cover ID to an existing book:
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":4,"method":"openlibrary.setCover","params":{"bookId":123,"coverId":240727}}'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":{}}'Start an app login attempt for Kindling:
curl -X POST http://localhost/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":30,"method":"auth.beginAppLogin","params":{"redirectUri":"kindling://auth/podible"}}'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.