Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs-developers/frontend/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@ handful of standalone screens.
| `/search` | `(app)/search.tsx` | Search across all connections, de-duplicated; shares its query text with the desktop top bar via `useSearchStore`. |
| `/library` | `(app)/library/index.tsx` | All libraries from all connections, plus a Favourites shelf row. |
| `/library/favourites` | `(app)/library/favourites.tsx` | The favourites list (un-heart in place). |
| `/library/[libraryId]` | `(app)/library/[libraryId]/index.tsx` | Library root browse - a two-line re-export of `src/components/library/browse-screen.tsx`. |
| `/library/[libraryId]/[...path]` | `(app)/library/[libraryId]/[...path].tsx` | Nested folder browse - same `BrowseScreen`, the catch-all segments become the folder `path` (helpers in `src/lib/paths.ts`). |
| `/book/[libraryId]/[...path]` | `(app)/book/[libraryId]/[...path].tsx` | Book detail: play/resume, download control, chapters, bookmarks, notes, listening history, other versions of the same book. |
| `/library/[libraryId]?connection=…&path=…` | `(app)/library/[libraryId].tsx` | Library browse, root and nested folders alike - a two-line re-export of `src/components/library/browse-screen.tsx`. Content routes are **flat**: the connection id and the library-relative folder `path` ride as query params, never as nested route segments (an in-app `router.push` cannot resolve a route nested under a dynamic layout segment - it lands on the group's first child; rationale and helpers in `src/lib/paths.ts`). The `(app)` layout republishes `?connection=` as the scope the content hooks read via `useScopedCid()`. |
| `/book/[libraryId]?connection=…&path=…` | `(app)/book/[libraryId].tsx` | Book detail: play/resume, download control, chapters, bookmarks, notes, listening history, other versions of the same book, and a capability-gated community-metadata section ([State & data](state-and-data.md#enriched-book-metadata)). Same flat query-param addressing as the library routes. |
| `/downloads` | `(app)/downloads.tsx` | Downloaded books + storage used ([Offline](offline.md)). |
| `/settings` | `(app)/settings.tsx` | Playback tunables, language, theme, connections, self-service password (the sign-out guard nudges a password-less user to set one via `sign-out-confirm.tsx`), per-server API keys (capability-gated), sign-out. |
| `/settings` | `(app)/settings.tsx` | App-level preferences only: playback tunables, language, theme, plus the Servers list that opens each connection's account screen. |
| `/account?connection=…` | `(app)/account.tsx` | Per-connection account screen, reached from the Settings screen's Servers list: set/change the self-service password (the sign-out guard nudges a password-less user here via `sign-out-confirm.tsx`), personal API keys (capability-gated, demo-hidden), and sign-out. |
| `/player` | `src/app/player.tsx` | The full player, presented as a full-screen modal above the shell. Accepts `libraryId`/`path` (+ optional `position`/`track`) params and gates playback start on the chapters query settling. |
| `/finished` | `src/app/finished.tsx` | The end-credits screen shown when a book finishes (or from the player's menu). A root modal sibling of the player; renders `EndCredits` with an "up next" suggestion. See [Playback](playback.md#ending-a-book-end-credits-and-up-next). |
| `/connect` layout | `src/app/connect/_layout.tsx` | Onboarding stack. An **authenticated** user is bounced home unless they are adding another server (`?add=1`, a pairing `?token=`, or a sign-in mid-flow via `pendingServerUrl`) - the app supports multiple simultaneous server connections. |
Expand Down
41 changes: 39 additions & 2 deletions docs-developers/frontend/state-and-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ servers at once, and two of them can each have a "library 1"): `qk.item(cid, lib
path)`, `qk.chapters(cid, lib, path)`, `qk.progress(cid, lib, path)`,
`qk.allProgress(cid)`, `qk.bookmarks/notes/history(cid, lib, path)`,
`qk.favourites(cid)`, `qk.libraries(cid)`, `qk.browse(cid, lib, path)`,
`qk.server(cid)` - so mutations can invalidate precisely and one server's cache
never shadows another's. Content keys are `(connectionId, libraryId, path)` tuples,
`qk.bookMeta(cid, lib, path)`, `qk.server(cid)` - so mutations can invalidate
precisely and one server's cache never shadows another's. Content keys are `(connectionId, libraryId, path)` tuples,
extending the path-is-identity rule across connections.

Patterns to copy when adding an endpoint:
Expand Down Expand Up @@ -105,6 +105,43 @@ where a result lives ("server · library") for de-duplicated rows.
queries when the server becomes reachable again - screens that errored or
emptied while offline repopulate without a remount.

### Enriched book metadata

The book screen (`src/app/(app)/book/[libraryId].tsx`) draws a
`BookMetaSection` (`src/components/library/book-meta.tsx`) beneath the
file/chapter list, in both the wide and phone branches, following the
section-per-concern pattern (the same shape as the bookmarks/notes sections). It
shows a description (collapsed past ~300 characters with a show-more toggle),
compact production details (publisher, release date, first published, an
"abridged" badge), a horizontal **more in this series** rail, and a quiet **View
on AudioSilo Meta** link.

Three details make it safe to add to a screen everyone sees:

- **Capability-gated.** The section mounts only when the server advertises the
`metadata` capability (`!!server.capabilities.metadata`, optional in
`types.ts` so an older server reads as absent → false). On a server without
the feature the query never fires.
- **Progressive enhancement.** The component renders **nothing** while loading,
on error, or on `{ matched: false }` - the page is never worse for having the
section, and its absence is indistinguishable from a book the metadata service
doesn't know.
- **Fetch tuned for a best-effort side dish.** `useBookMeta` keys on
`qk.bookMeta(cid, lib, path)` with a **1 h `staleTime`** (the server caches the
composed envelope too, so re-fetching sooner buys nothing) and **`retry: false`**
so a 502 from a down metadata service resolves once and stops - it must not
spin or block the rest of the screen.

`client.bookMeta(libraryId, path, signal)` calls `GET /libraries/{id}/meta`; the
`BookMeta` discriminated union (`{ matched: false } | { matched: true; work;
recording?; series?; web_url }`) in `types.ts` is hand-mirrored from the server's
envelope (the mirroring rule above applies - a change to the shape is a two-repo
change with tests on both sides). Every series-rail entry
carries its own `web_url`, so tapping a series work or the footer link opens the
metadata site **externally** (a real new tab on web, an in-app browser tab on
native) - the client never constructs a metadata URL. UI strings live under
`book.meta.*` in the locale catalogs.

## Zustand stores

### Session (`src/stores/session.ts`)
Expand Down
7 changes: 5 additions & 2 deletions docs-developers/server/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ a `limit` and no pagination.
"transcode": true,
"upload": false,
"websocket": false,
"api_keys": true
"api_keys": true,
"metadata": true
},
"auth": { "methods": ["auth_code", "password"] },
"demo": { "enabled": false }
Expand All @@ -207,7 +208,9 @@ a `limit` and no pagination.
Clients **must** feature-gate on these flags rather than probing endpoints:
`transcode` reflects whether ffmpeg is configured (without it, `?transcode=1`
is 503), `web_player` whether `/web` is mounted, `api_keys` whether the server
supports user-minted [API keys](reference.md#personal-api-keys),
supports user-minted [API keys](reference.md#personal-api-keys), `metadata`
whether [community metadata lookup](reference.md#get-apiv1librariesidmeta) is
configured (the enriched-book section is drawn only when it is true),
`upload`/`websocket` are roadmap phases that will flip on when they land.
`demo.enabled` drives the "Try the demo" affordance.

Expand Down
95 changes: 93 additions & 2 deletions docs-developers/server/api/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ else and gate features on the flags.
"transcode": true,
"upload": false,
"websocket": false,
"api_keys": true
"api_keys": true,
"metadata": true
},
"auth": { "methods": ["auth_code", "password"] },
"demo": { "enabled": false }
Expand All @@ -52,7 +53,10 @@ it the moment it pairs. `version` is stamped from the release tag (`"dev"` for l
`transcode` is true only when ffmpeg is configured; `web_player` only when the
`/web` mount is populated; `upload` and `websocket` are reserved for future
phases and currently always false. `api_keys` is true on servers that support
user-minted [personal API keys](#personal-api-keys).
user-minted [personal API keys](#personal-api-keys). `metadata` is true when the
server has [community metadata lookup](#get-apiv1librariesidmeta) configured
(`metadata.enabled` and a `metadata.base_url`); clients gate the enriched-book
section on it.

### `GET /healthz` · `GET /api/v1/healthz`

Expand Down Expand Up @@ -662,6 +666,93 @@ multi-file mp3 parts render identically.

Same status codes as `/item`.

### `GET /api/v1/libraries/{id}/meta`

*Session.* Community metadata enrichment for a book - a description, production
details, and the series it belongs to - composed server-side from the community
metadata API ([meta.audiosilo.app](https://meta.audiosilo.app)) and cached. The
path is authorized against the caller's share scope exactly like `/item`. Gated
by the `metadata` [capability](#get-apiv1server): when it is false the route
returns 404, so a client that honours the flag never calls this.

The server resolves the book's `asin`/`isbn` (the fields on the indexed book,
backfilled by the manager into `book_enrichment`), looks the recording up
upstream, and folds the matched recording plus up to three series rails into one
envelope. Every series-rail entry carries its own `web_url`, so a client links to
the metadata site without ever building a URL itself.

| Query param | Type | Required |
|---|---|---|
| `path` | string | yes |

Response `200` on a match:

```json
{
"matched": true,
"work": {
"id": "the-martian",
"title": "The Martian",
"subtitle": "",
"authors": [{ "id": "andy-weir", "name": "Andy Weir" }],
"language": "en",
"first_published": "2011",
"description": "An astronaut is stranded on Mars…"
},
"recording": {
"id": "podium-2013",
"narrators": [{ "id": "r-c-bray", "name": "R. C. Bray" }],
"abridged": false,
"runtime_min": 634,
"release_date": "2013-03-22",
"publisher": "Podium Audio",
"cover_url": "https://…"
},
"series": [
{
"id": "wandering-earth",
"name": "The Wandering Earth",
"position": "1",
"works": [
{
"id": "the-martian",
"title": "The Martian",
"position": "1",
"authors": [{ "id": "andy-weir", "name": "Andy Weir" }],
"cover_url": "https://…",
"web_url": "https://meta.audiosilo.app/work?id=the-martian"
}
]
}
],
"web_url": "https://meta.audiosilo.app/work?id=the-martian"
}
```

`work` is always present on a match; `recording` and `series` are omitted when
the upstream has none, and every `omitempty` string/number field (`subtitle`,
`first_published`, `description`, `abridged`, `runtime_min`, `release_date`,
`publisher`, `cover_url`) is dropped when empty. `series[].position` is this
work's position in that series; `series[].works` is the full ordered rail,
**including the current work** (the client filters it out before drawing a "more
in this series" row). Positions are strings ("1", "2.5", "1-3.5").

When the book has neither an `asin` nor an `isbn`, or the upstream reports no
match, the response is `200 { "matched": false }` - a normal, non-error result
that the client treats as "nothing to show":

```json
{ "matched": false }
```

| Status | Meaning |
|---|---|
| `200` | `{ "matched": true, … }` on a match, or `{ "matched": false }` when there is nothing to show |
| `400` | missing `path` / invalid library id |
| `403` | path outside the caller's share scope |
| `404` | `no book at that path`, **or** metadata lookup is disabled on this server (`metadata` capability false) |
| `502` | `metadata service unavailable` - the upstream was unreachable or errored |

## Streaming & media

Both routes take *media auth* (header **or** `?token=`) and are exempt from the
Expand Down
25 changes: 24 additions & 1 deletion docs-developers/server/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,26 @@ case they live only in the database. There is deliberately **no layout key**
| `demo.max_users` | int (optional), unset | Cap on concurrent live demo accounts. **Unset → safe default 200** (`config.DefaultDemoMaxUsers`); **explicit `0` = unlimited** (opt-in risk: per-IP creation limits are bypassable by rotating IPs) |
| `demo.idle_ttl` | duration string, `"24h"` | Reap demo accounts idle longer than this (background reaper sweeps every 15 min). Must parse as a positive `time.Duration`; empty falls back to 24h |

### Community metadata (`metadata.*`)

The server can enrich a book with a description, production details and its
series by resolving the book's ASIN/ISBN against the community metadata API
([meta.audiosilo.app](https://meta.audiosilo.app)) and serving the result at
`GET /libraries/{id}/meta` (players draw it beneath the chapter list). The
lookup is server-side by design - one cached seam, and one config key that turns
off all outbound calls.

| Key | Type / default | Meaning |
|---|---|---|
| `metadata.enabled` | bool, `true` | Turn the metadata lookup on. When `false`, the server makes **no outbound metadata calls**, `GET /libraries/{id}/meta` returns 404, and the `metadata` capability reports false so players hide the enriched-book section entirely |
| `metadata.base_url` | string, `"https://meta.audiosilo.app"` | Base URL of the metadata service (the site is served at `/` and the API at `/api/v1`). **Must be an absolute `http`/`https` URL when metadata is enabled** |

Turning it off is the one-key privacy switch: with `metadata.enabled: false`
the server never contacts the metadata service, and every player connected to it
stops showing the section (they gate on the `metadata` capability). Enrichment is
strictly additive and cached - a slow or unreachable service degrades to no
section, never a broken page.

## Environment variables

`applyEnv` overrides a fixed set of keys from `AUDIOSILO_*` variables - this
Expand All @@ -128,6 +148,8 @@ is the complete list (anything not here, e.g. `app_links`, `libraries`,
| `AUDIOSILO_DEMO_LIBRARY` | `demo.library` | string |
| `AUDIOSILO_DEMO_MAX_USERS` | `demo.max_users` | integer (`0` = unlimited) |
| `AUDIOSILO_DEMO_IDLE_TTL` | `demo.idle_ttl` | Go duration, e.g. `24h` |
| `AUDIOSILO_METADATA_ENABLED` | `metadata.enabled` | `strconv.ParseBool` (`true`/`1`/…) |
| `AUDIOSILO_METADATA_BASE_URL` | `metadata.base_url` | URL |

List values are split on commas with whitespace trimmed and empties dropped
(`splitList`). Numeric/boolean variables that fail to parse are **silently
Expand All @@ -144,7 +166,8 @@ overrides). It rejects:
- any `trusted_proxies` entry that isn't a valid CIDR;
- a library with an empty name or root, or a duplicate library name;
- demo mode without `demo.library`; a `demo.idle_ttl` that doesn't parse or
isn't positive (rejected loudly rather than silently replaced by 24h).
isn't positive (rejected loudly rather than silently replaced by 24h);
- metadata enabled with an empty or non-absolute-`http(s)` `metadata.base_url`.

Secure-by-default choices baked into `Default()` and first-run: TLS on
(`selfsigned`) out of the box, no default passwords (credentials are minted
Expand Down
10 changes: 10 additions & 0 deletions docs-users/listening/browsing.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ Tap a book to open its detail page:

If the same book exists in more than one place (say, an M4B copy and an MP3 copy), the detail page offers **Choose a version** so you can pick which copy to play.

### About this book

Some books show an extra section below the chapters with more about the title:

- An **About** description of the story, and production details such as the **publisher**, **release date** and when the work was **first published**.
- A **More in this series** row - other books in the same series, each opening its page on the metadata site so you can see what to read next.
- A **View on AudioSilo Meta** link to the book's full entry.

This information comes from the **AudioSilo community metadata database** at [meta.audiosilo.app](https://meta.audiosilo.app), a free, community-run catalogue of audiobook details. It appears only when the book can be matched (it carries an ASIN or ISBN) and your server has the metadata lookup switched on. If a book has no extra section, it simply isn't matched or your admin has turned the feature off - nothing else on the page changes. A server admin can disable the lookup entirely, in which case no book shows this section (see the metadata setting in the [configuration reference](/developers/server/configuration)).

## Search

The **Search** tab (or the search box in the desktop header) searches **titles, authors, and series** across every library you have access to - and across every server you've added:
Expand Down
Loading