This document is a build plan for a new Kindling backend that replaces LL. It is written so an agent can implement the entire system end-to-end with realistic tests.
- Provide a reliable, deterministic backend for Kindling (macOS + iOS).
- Keep search, snatch, download, import, streaming, and feed generation in one service.
- Make the system resilient to duplicate events, partial failures, and provider quirks.
- Provide a realistic test harness with mocked Torznab and rTorrent so regressions are caught early.
- Maintain LL feature parity or LL-specific semantics.
- Support every provider or every downloader. Initial support: Torznab + rTorrent only.
- Cloud sync. Design with future CloudKit support in mind, but do not implement now.
Process layout:
- HTTP API server (Express)
- Background worker loop (jobs)
- SQLite storage (node:sqlite)
- Optional file watcher (library root scan)
- Streaming + feeds endpoints (podible behavior preserved)
Key subsystems:
- Search: Torznab query + normalize results
- Snatch: create Release + enqueue download job
- Download: rTorrent integration + status polling
- Import: detect files, compute assets, link into library, update book
- Streaming + feed: range streaming, chapters, feeds
- Node.js 24+ with experimental transform-types for TypeScript
- Express for HTTP routing
- node:sqlite for storage
- node:child_process for rTorrent/ffmpeg/ffprobe
- No version prefix in URLs
Tables:
- id TEXT PRIMARY KEY
- title TEXT NOT NULL
- author TEXT NOT NULL
- status TEXT NOT NULL (open|snatched|downloading|downloaded|imported|error)
- primary_asset_id TEXT NULL
- cover_path TEXT NULL
- duration_ms INTEGER NULL
- added_at TEXT NOT NULL
- published_at TEXT NULL
- description TEXT NULL
- description_html TEXT NULL
- language TEXT NULL
- isbn TEXT NULL
- identifiers_json TEXT NULL
- id TEXT PRIMARY KEY
- book_id TEXT NOT NULL
- provider TEXT NOT NULL
- title TEXT NOT NULL
- info_hash TEXT NULL
- size_bytes INTEGER NULL
- url TEXT NOT NULL
- snatched_at TEXT NOT NULL
- status TEXT NOT NULL (snatched|downloading|downloaded|imported|failed)
- error TEXT NULL
- FOREIGN KEY(book_id) REFERENCES books(id)
- id TEXT PRIMARY KEY
- book_id TEXT NOT NULL
- kind TEXT NOT NULL (single|multi)
- mime TEXT NOT NULL
- total_size INTEGER NOT NULL
- duration_ms INTEGER NULL
- active INTEGER NOT NULL DEFAULT 0
- source_release_id TEXT NULL
- created_at TEXT NOT NULL
- FOREIGN KEY(book_id) REFERENCES books(id)
- FOREIGN KEY(source_release_id) REFERENCES releases(id)
- id TEXT PRIMARY KEY
- asset_id TEXT NOT NULL
- path TEXT NOT NULL
- size INTEGER NOT NULL
- start INTEGER NOT NULL
- end INTEGER NOT NULL
- duration_ms INTEGER NOT NULL
- title TEXT NULL
- FOREIGN KEY(asset_id) REFERENCES assets(id)
- id TEXT PRIMARY KEY
- type TEXT NOT NULL (scan|search|snatch|download|import|transcode|reconcile)
- status TEXT NOT NULL (queued|running|succeeded|failed|cancelled)
- book_id TEXT NULL
- release_id TEXT NULL
- payload_json TEXT NULL
- error TEXT NULL
- created_at TEXT NOT NULL
- updated_at TEXT NOT NULL
- id TEXT PRIMARY KEY
- key TEXT NOT NULL UNIQUE
- status TEXT NOT NULL (started|succeeded|failed)
- created_at TEXT NOT NULL
- updated_at TEXT NOT NULL
Idempotency keys should be derived as book_id + info_hash + action where possible, or include a stable provider URL.
Books transition through:
open -> snatched -> downloading -> downloaded -> imported
Rules:
- Transitions are monotonic. No backward transition unless explicit user action.
- Failures do not erase last known good state. Store
erroron release/job. - Multiple assets per book are allowed.
activeasset is for playback and feeds.
Release transition mirrors book but can fail independently:
snatched -> downloading -> downloaded -> imported or failed
Idempotency:
- All external-triggered actions (snatch/download/import) must be idempotent.
- Use operations table to block duplicate work.
- Deduplicate by infohash or url before contacting rTorrent.
GET /healthGET /server
GET /library?limit=&cursor=&q=GET /library/{bookId}POST /library/refreshGET /library/changes?since=
POST /search-> returns normalized Torznab resultsPOST /snatch-> creates release and download jobGET /releases?bookId=
GET /downloads-> mapped from jobs/releasesGET /downloads/{id}POST /downloads/{id}/retryPOST /import/reconcileGET /assets?bookId=POST /assets/{assetId}/activate
PUT /playback/positionGET /playback/positions?since=
GET /stream/{assetId}.{ext}(range supported)GET /chapters/{assetId}.jsonGET /covers/{bookId}.jpgGET /feed.xmlGET /feed.json
GET /settingsPUT /settings
- Endpoint:
GET /api?t=search&q=...ort=search&cat=... - Parse RSS/Atom results
- Normalize fields: title, size, download url, provider, seed/leech
Use XML-RPC or rpc interface. Needed calls:
load.start(or equivalent) with magnet/urld.name,d.hash,d.complete,d.get_base_pathd.get_bytes_done,d.get_size_bytes
- Search by title+author or ISBN
- Fetch metadata and cover
- Must be optional: import should not fail on missing metadata
- Do not delete or move source torrents
- Create hardlink when same filesystem, reflink when supported, else symlink
- Compute asset(s) as first-class objects
- Replace means swapping active asset, not deleting history
Maintain the behaviors from ../podible:
- Single-file m4b can be transcoded to mp3
- Multi-mp3 is stitched with correct range handling
- ID3 chapter tag injection for multi assets
- Xing header patching for concatenated streams
- JSON feed + RSS feed with cover/chapters
- State machine transitions
- Idempotency key behavior
- Asset construction from file layout
Use local mock services:
-
Mock Torznab
- Express server with static RSS responses
- Variants: valid results, empty results, malformed response
-
Mock rTorrent
- Express XML-RPC endpoint that simulates:
- success
- duplicate hash
- timeout
- complete state changes over time
- Express XML-RPC endpoint that simulates:
-
File fixtures
- Single m4b
- Multi mp3 book
- Mismatched metadata
- Search -> snatch -> download -> import -> asset active -> stream
- Duplicate snatch attempt should be idempotent
- rTorrent timeout should not corrupt state
- Reconcile should recover downloaded-but-not-imported
node --test --experimental-transform-types- Start mock services during tests
- Use tmp dirs for filesystem side effects
- Storage + migrations + schema
- Search + Torznab normalization
- Snatch + rTorrent integration
- Import pipeline + asset creation
- Streaming + feeds
- Mock services + tests
- API server with stable behavior
- Mock services in
test/mocks - E2E test suite runnable in CI
- README with setup instructions