Stats docs written handler/service-first (summary/this-week/book-stats
new; library-usage frontmatter'd — its body was already accurate).
Registry: 4 entries on services.UserSummaryStats/WeekSummaryStats/
LibraryUsageStats/BookStats. Family-lens note: library-usage 403s an
explicit library_id outside the visible set (431fec0 rule).
UNVERIFIED sweep (7 -> 0), each resolved against the handler:
- device-usage takes NO date params; popular-books takes only ?limit
(the 'standard analytics date-range params' claim was wrong)
- link_book media_item_id: no validate tag but service-always-written
- push_stats book-ref field set documented (uuid/sha256/title/authors)
- filter_count: confirmed ParseMediaSearchParams machinery + 400 on bad
filter + 403 visibility
- facets values element shape pinned: {value, display?, count}
- get_config: dynamic key set IS the contract (reworded, no marker)
make test 9/9.
5.1 KiB
method, path, auth, area
| method | path | auth | area |
|---|---|---|---|
| GET | /api/stats/library-usage | jwt | stats |
Get Library Usage Statistics
The /stats page's Library tab payload: media_items inventory
aggregates (what is IN the library), not reading behavior. Mirrors the
Audiobookshelf library dashboard's information architecture.
Endpoint: GET /api/stats/library-usage
Auth: Required (any authenticated user)
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| library_id | string (UUID) | No | Scope to one library. Omit to aggregate across ALL of the requesting user's visible libraries. |
Family-lens rule
Every response is scoped to the requester's visible-library set
(library_visibility, default-ALLOW). An explicit library_id OUTSIDE
that set is a 403 — never a silent pass. Omitted library_id
aggregates only visible libraries.
Example Request
GET /api/stats/library-usage?library_id=8c8d0cee-1cca-4def-9a37-edad3a4d438b
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Response (200 OK)
{
"library_id": "8c8d0cee-1cca-4def-9a37-edad3a4d438b",
"items": 743,
"authors": 446,
"narrators": 41,
"total_size_bytes": 309237452800,
"total_seconds": 291240,
"total_pages": 0,
"hours_mode": true,
"genres": [
{ "name": "Fantasy", "count": 142 }
],
"top_authors": [
{ "name": "Terry Pratchett", "count": 42 }
],
"longest_items": [
{
"media_item_id": "uuid",
"title": "Gardens of the Moon",
"author": "Steven Erikson",
"cover_image_path": "/uploads/8c8d0cee/cover.jpg",
"seconds": 93600,
"pages": 0,
"size": 1400000000
}
],
"largest_items": [
{
"media_item_id": "uuid",
"title": "Gardens of the Moon",
"author": "Steven Erikson",
"cover_image_path": "/uploads/8c8d0cee/cover.jpg",
"seconds": 93600,
"pages": 0,
"size": 1400000000
}
]
}
Field semantics
| Field | Meaning |
|---|---|
library_id |
"" when the scope is the All-libraries aggregate. |
items |
Healthy media items (archived / missing-scan rows never count). |
authors |
DISTINCT non-empty author values. |
narrators |
DISTINCT values unnested from the narrators TEXT[] column across the scope. |
total_size_bytes |
SUM of file_size. |
total_seconds |
SUM of audio_metadata.duration_seconds — real audio hours; 0 for text libraries. |
total_pages |
SUM of page_count — meaningful for text libraries; 0 for audiobooks. |
hours_mode |
true when audio-duration data exists in scope: the client renders the time card in HOURS and ranks Longest Items by seconds. false: the time card renders in PAGES and Longest Items ranks by pages. The client decides — the payload always carries both sums. |
genres |
Top 5 non-empty genre by item count (ties break alphabetically). |
top_authors |
Top 10 non-empty author by item count (ties break alphabetically). |
longest_items |
Top 5 by seconds (hours mode) or pages (pages mode). |
largest_items |
Top 5 by file_size — works for every library type. |
Absent data
Audio Tracks (Audiobookshelf's per-file count) is deliberately absent:
multi-file audiobooks (folder-of-tracks / zip books) now ingest as ONE
item whose track list lives in audio_metadata.tracks, so a file count
would still not equal a per-book "tracks" metric without a new query.
Add it by aggregating jsonb_array_length(audio_metadata->'tracks') if
the panel is ever wanted.
Error Responses
| Code | Description |
|---|---|
| 400 | library_id is not a valid UUID |
| 401 | Invalid or expired token |
| 403 | Library not in the user's visible set |
| 500 | Aggregation failure |