bookshelf: its config dir is placed, its image is the one ace runs #163

Open
mesh-admin wants to merge 2 commits from feat/bookshelf-for-ace into main
Contributor

The config directory was the stated path /services/bookshelf/config; it
is now a pathless directory the mesh places (0700, 1000:1000), and the
route binding lives in a placed state dir, as in jackett and searxng.
/var/lib/mesh/bookshelf stays for the broker secret.

The image is pinned to the digest ace runs (hardcover, 0.4.21.182,
ls36). The old pin was a February build: older than the database it
would open, which Readarr-family apps migrate forward only.

The sidecar dials ${port:8787}, the port the mesh assigned, not the
software's own (the same one-line change as #154).

Not carried from HAL: its install hook seeded config.xml with an API key
(the image writes its own on first start, and the data keeps it) and
registered nzbget, qbittorrent and jackett through the API with wrong
hosts and ports, so ace's bookshelf has no download client and no
indexer today. Wiring them is provisioning work that depends on the
download-client and jackett providers, and on where ace's books and
downloads live (hq 153).

Verified: catalogue tests with MESH_CATALOGUE; a scratch resolution with
ace's assignment composes books.zurag.be and keeps the port to the
mesh; a throwaway container at the pinned digest on a fresh 0700
1000:1000 dir answered /ping and /api/v1/system/status (401 on a wrong
key); client.ts typechecks strict, found the key in config.xml and read
the queue.

The assignment draft is ace-assignments/bookshelf.json: web, label books, public. The download-client and indexer provisioning, and where ace's books and downloads live, are left open for the operator (hq 153).

The config directory was the stated path /services/bookshelf/config; it is now a pathless directory the mesh places (0700, 1000:1000), and the route binding lives in a placed state dir, as in jackett and searxng. /var/lib/mesh/bookshelf stays for the broker secret. The image is pinned to the digest ace runs (hardcover, 0.4.21.182, ls36). The old pin was a February build: older than the database it would open, which Readarr-family apps migrate forward only. The sidecar dials ${port:8787}, the port the mesh assigned, not the software's own (the same one-line change as #154). Not carried from HAL: its install hook seeded config.xml with an API key (the image writes its own on first start, and the data keeps it) and registered nzbget, qbittorrent and jackett through the API with wrong hosts and ports, so ace's bookshelf has no download client and no indexer today. Wiring them is provisioning work that depends on the download-client and jackett providers, and on where ace's books and downloads live (hq 153). Verified: catalogue tests with MESH_CATALOGUE; a scratch resolution with ace's assignment composes books.zurag.be and keeps the port to the mesh; a throwaway container at the pinned digest on a fresh 0700 1000:1000 dir answered /ping and /api/v1/system/status (401 on a wrong key); client.ts typechecks strict, found the key in config.xml and read the queue. The assignment draft is ace-assignments/bookshelf.json: web, label books, public. The download-client and indexer provisioning, and where ace's books and downloads live, are left open for the operator (hq 153).
mesh-admin added 1 commit 2026-09-30 09:58:49 +00:00
The config directory was the stated path /services/bookshelf/config; it
is now a pathless directory the mesh places (0700, 1000:1000), and the
route binding lives in a placed state dir, as in jackett and searxng.
/var/lib/mesh/bookshelf stays for the broker secret.

The image is pinned to the digest ace runs (hardcover, 0.4.21.182,
ls36). The old pin was a February build: older than the database it
would open, which Readarr-family apps migrate forward only.

The sidecar dials ${port:8787}, the port the mesh assigned, not the
software's own (the same one-line change as #154).

Not carried from HAL: its install hook seeded config.xml with an API key
(the image writes its own on first start, and the data keeps it) and
registered nzbget, qbittorrent and jackett through the API with wrong
hosts and ports, so ace's bookshelf has no download client and no
indexer today. Wiring them is provisioning work that depends on the
download-client and jackett providers, and on where ace's books and
downloads live (hq 153).

Verified: catalogue tests with MESH_CATALOGUE; a scratch resolution with
ace's assignment composes books.zurag.be and keeps the port to the
mesh; a throwaway container at the pinned digest on a fresh 0700
1000:1000 dir answered /ping and /api/v1/system/status (401 on a wrong
key); client.ts typechecks strict, found the key in config.xml and read
the queue.
jschoubben added 1 commit 2026-09-30 11:08:09 +00:00
bookshelf reached its download clients as nzbget:6789 and qbittorrent:8112 and its indexers
through jackett:9117 or indexers.zurag.be - HAL container names and a public route,
typed into its database by hand. Nothing on the mesh answers those names.

It now requires nzbget-api, qbittorrent-api and jackett-api. A run-once step
(downloads/, declared last, restart-on its bindings, credentials and settings) writes
host, port, TLS, base path, user and credential into bookshelf through bookshelf's own API:

- A download client is the mesh's when its name is downloads.<provision>.name and its
  kind the provider's; it is registered when missing. A jackett feed is the mesh's when
  its host is the bound one, one the step bound before, or one in
  downloads.jackett-api.adopt-hosts; the jackett indexers in
  downloads.jackett-api.indexers are registered when missing. Every other entry is left
  alone, and nothing is ever deleted.
- Only connection fields, only when they differ. The stored password is masked, so the
  app tests the entry with the credential it holds; only if that fails is the delivered
  one written. Categories, priorities and "enabled" are never touched.
- Each credential is tried against its provider first. A minted value (nothing accepted
  yet) is never written; the step exits 1 naming the exact secret accept.

The step is byte-identical in sonarr, radarr, lidarr and bookshelf (the same Servarr
API, v3 or v1): each module builds from its own directory, so each carries a copy, and
test/downloads.test.ts fails if a sibling's copy differs.

Verified: strict typecheck, the Dockerfile build, 14 unit tests; and the compiled step
against fresh pinned sonarr/radarr/lidarr/bookshelf with throwaway nzbget, qBittorrent
and jackett - minted credentials refused with nothing written, accepted ones registered
and tested by the app, a migration-shaped radarr repointed, reruns unchanged.
Author
Contributor

New commit bd0c6b3: bookshelf reaches nzbget, qbittorrent and jackett through the mesh.

Manifest

  • Now requires nzbget-api (#167), qbittorrent-api (#168) and jackett-api (#150).
  • The bindings and pair credentials land in ${dir:state}.
  • New resources:
    • downloads-config: ${dir:state}/downloads.json, merge json, carrying node: ${machine:name} and the downloads.* settings.
    • downloads-memory: a directory.
    • downloads: a run-once step on the host network, declared last. It restarts when the settings, any of the three bindings or any of the three secrets change.

What the step does, through bookshelf's own API:

  • It writes the connection fields only (host, port, TLS, base path, user, credential) into the entries the mesh manages:
    • Download clients: the entry whose name is downloads.<provision>.name and whose kind matches that provider. If there is none, the step registers one.
    • jackett feeds: a Torznab feed of jackett's shape whose host is one of: the bound at, a host the step pointed feeds at before (kept in downloads-memory), or a host listed in downloads.jackett-api.adopt-hosts. A jackett indexer listed in downloads.jackett-api.indexers that the app lacks is registered.
  • Everything else is left alone: other entries, categories, priorities and "enabled". Nothing is ever deleted.
  • The stored password is masked, so the step asks bookshelf to test the entry with the password it already holds. The delivered credential is written only if that test fails.
  • Each credential is tried against its provider first. A minted value is never written: the step exits 1 with secret accept <node> bookshelf <provision> --provider <node> --from <file>.
  • Shared code: the step is byte-identical in sonarr, radarr, lidarr and bookshelf, which use the same Servarr API (v3 or v1). Each module builds from its own directory, so each carries a copy, and test/downloads.test.ts fails if a sibling's copy differs.

On ace: bookshelf has no download clients or indexers today, so no adoption setting is needed. The step registers nzbget and qbittorrent. If ace's nzbget has no Readarr category, the nzbget entry is registered with its category left empty, and the step says so. No indexers are registered until downloads.jackett-api.indexers lists some; which jackett indexers suit books is the operator's choice.

Verified

  • Strict typecheck and the Dockerfile build pass, and 14 unit tests pass.
  • Catalogue tests pass with MESH_CATALOGUE set to all seven downloads branches merged. A scratch resolve on a fake ace filled every ${bound}, ${port}, ${machine}, ${dir} and each consumer's own sealed credential, and the step is the module's last container.
  • End-to-end run of the compiled step against the fresh pinned app, with throwaway nzbget, qBittorrent and jackett:
    • Minted credentials were refused, with the exact accept command, and nothing was written.
    • Accepted credentials registered the clients and a feed, and the app's own test passed.
    • A radarr shaped like ace's (NZBGet at nzbget:6789, qBitTorrent with an old password, disabled feeds at jackett:9117 including one dead one) was repointed.
    • A person's seedbox feed was left untouched, and reruns reported "already as the mesh says".
**New commit bd0c6b3: bookshelf reaches nzbget, qbittorrent and jackett through the mesh.** **Manifest** - Now requires `nzbget-api` (#167), `qbittorrent-api` (#168) and `jackett-api` (#150). - The bindings and pair credentials land in `${dir:state}`. - New resources: - `downloads-config`: `${dir:state}/downloads.json`, merge json, carrying `node: ${machine:name}` and the `downloads.*` settings. - `downloads-memory`: a directory. - `downloads`: a run-once step on the host network, declared last. It restarts when the settings, any of the three bindings or any of the three secrets change. **What the step does**, through bookshelf's own API: - It writes the connection fields only (host, port, TLS, base path, user, credential) into the entries the mesh manages: - **Download clients:** the entry whose name is `downloads.<provision>.name` and whose kind matches that provider. If there is none, the step registers one. - **jackett feeds:** a Torznab feed of jackett's shape whose host is one of: the bound `at`, a host the step pointed feeds at before (kept in `downloads-memory`), or a host listed in `downloads.jackett-api.adopt-hosts`. A jackett indexer listed in `downloads.jackett-api.indexers` that the app lacks is registered. - **Everything else is left alone:** other entries, categories, priorities and "enabled". **Nothing is ever deleted.** - **The stored password is masked,** so the step asks bookshelf to test the entry with the password it already holds. The delivered credential is written only if that test fails. - **Each credential is tried against its provider first.** A minted value is never written: the step exits 1 with `secret accept <node> bookshelf <provision> --provider <node> --from <file>`. - **Shared code:** the step is byte-identical in sonarr, radarr, lidarr and bookshelf, which use the same Servarr API (v3 or v1). Each module builds from its own directory, so each carries a copy, and `test/downloads.test.ts` fails if a sibling's copy differs. **On ace:** bookshelf has no download clients or indexers today, so no adoption setting is needed. The step registers `nzbget` and `qbittorrent`. If ace's nzbget has no `Readarr` category, the nzbget entry is registered with its category left empty, and the step says so. No indexers are registered until `downloads.jackett-api.indexers` lists some; which jackett indexers suit books is the operator's choice. **Verified** - Strict typecheck and the Dockerfile build pass, and 14 unit tests pass. - Catalogue tests pass with `MESH_CATALOGUE` set to all seven downloads branches merged. A scratch resolve on a fake ace filled every `${bound}`, `${port}`, `${machine}`, `${dir}` and each consumer's own sealed credential, and the step is the module's last container. - End-to-end run of the compiled step against the fresh pinned app, with throwaway nzbget, qBittorrent and jackett: - Minted credentials were refused, with the exact accept command, and nothing was written. - Accepted credentials registered the clients and a feed, and the app's own test passed. - A radarr shaped like ace's (`NZBGet` at `nzbget:6789`, `qBitTorrent` with an old password, disabled feeds at `jackett:9117` including one dead one) was repointed. - A person's seedbox feed was left untouched, and reruns reported "already as the mesh says".
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/bookshelf-for-ace:feat/bookshelf-for-ace
git checkout feat/bookshelf-for-ace
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-catalog#163