Skip to content

Library Statistics

3 endpoints.

GEThttp://localhost:3000/api/stats/bucket

A drill-down: given one bucket of one /stats chart, the archives in it, for the list the page shows under the chart. Fetched per click rather than folded into /stats/overview, which is a whole-library snapshot that would have to carry every bucket’s archive list to support this.

Deliberately not expressed as a search query, even though some of these buckets have one: “last read on day X” has no search operator at all, and the progress ladder’s unread rung means “never opened” (its own is_read) while the search’s read:/is:unread filters key on the progress field alone — so they disagree about an archive that was opened without a page turn. Deriving the list from the same classification the chart used is what keeps the list and the bar consistent by construction.

Requesting this as a guest is scoped to what that guest may see, matching /stats/overview.

Query parameters

metricstring · enum: ingest | read | size | rating | progressrequired
ingest/read are time buckets and need from+to; size/rating/progress are keyed buckets and need key.
fromstringoptional
ingest/read only. First day of the bucket, YYYY-MM-DD, inclusive. The client sends the span it actually drew rather than a unit+key pair, so no bucket-key-to-span arithmetic has to exist in two languages.
tostringoptional
ingest/read only. Last day of the bucket, YYYY-MM-DD, inclusive.
keystringoptional
size bucket key (lt_1mb … gte_500mb), a rating of 0-5 (where 0 is the unrated-or-zero slot, exactly as the chart's own distribution counts it), or a progress ladder key (unread, 1_20, 21_40, 41_60, 61_80, 81_99, finished).

Responses

200The bucket's archives, newest/largest/most-progressed first per metric.
{
  "archives": [
    null
  ],
  "metric": "ingest",
  "total": 0,
  "truncated": false
}
400Unknown metric, or a missing/invalid `from`/`to`/`key` for that metric.
{
  "error": "",
  "operation": "",
  "success": 0,
  "successMessage": ""
}

Your own instance address. Requests go from this browser straight to it.

GEThttp://localhost:3000/api/stats/overview

One snapshot covering: headline totals, archive size distribution, container format/MIME breakdown, ingestion history (by month, by ingestion path, and the newest arrivals), reading history (per-day, progress distribution, ratings, and the most recently/most often read works), and a weighted reading-preference analysis over tags, authors and categories.

Requesting this as a guest yields counts and breakdowns restricted to what that guest may actually see, matching /database/stats’s own scoping.

Weighted preference values are relative rankings, not probabilities or percentages: they are only ever meaningful against each other within one response.

Responses

200The aggregated snapshot.
{
  "formats": [
    null
  ],
  "generated_at": 0,
  "ingest": {
    "recent": null,
    "sources": null,
    "sources_sampled": null,
    "sources_sampled_only": null
  },
  "note": "no_server_side_reading",
  "preferences": {
    "by_namespace": null,
    "categories": null,
    "categories_truncated": null,
    "highlights": null,
    "namespaces": null,
    "ratings": null,
    "read_archives": null
  },
  "reading": {
    "most_reread": null,
    "progress": null,
    "ratings": null,
    "recent": null
  },
  "size_buckets": [
    null
  ],
  "totals": {
    "archives": null,
    "average_bytes": null,
    "average_pages": null,
    "bookmarks": null,
    "categories": null,
    "finished": null,
    "largest_bytes": null,
    "new": null,
    "pages": null,
    "pages_read": null,
    "rated": null,
    "reading": null,
    "storage_bytes": null,
    "tankoubons": null,
    "unread": null
  }
}

Your own instance address. Requests go from this browser straight to it.

GEThttp://localhost:3000/api/stats/timeline

The page’s time-series source, kept separate from /stats/overview because it is driven interactively: the overview is one expensive snapshot (its preference analysis runs a search per dynamic category), while this endpoint is refetched per range change and re-bucketed entirely client-side.

The series is always daily, whatever granularity the reader is looking at, so that changing granularity — or zooming and panning — needs no request at all once the range has been fetched once. A request wider than the server’s own cap is clamped to the newest days of it, and available_from still reports where the data really begins.

Requesting this as a guest is scoped to what that guest may see, matching /stats/overview.

Query parameters

metricstring · enum: ingest | readingrequired
ingest counts archives added per day (from each archive's own date_added tag); reading reports page turns and archives-last-read per day.
fromintegeroptional
Unix seconds. Defaults to the whole available range — which is what the page asks for, so every later zoom is local. Clamped to the library's own data.
tointegeroptional
Unix seconds; defaults to now, and is never later than it.

Responses

200The daily series, oldest first.
{
  "available_from": "",
  "available_to": "",
  "from": "",
  "metric": "ingest",
  "pages_since": "",
  "points": [
    null
  ],
  "to": ""
}
400`metric` was missing or was neither `ingest` nor `reading`.
{
  "error": "",
  "operation": "",
  "success": 0,
  "successMessage": ""
}

Your own instance address. Requests go from this browser straight to it.