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.
115 lines
5.1 KiB
Markdown
115 lines
5.1 KiB
Markdown
---
|
|
method: GET
|
|
path: /api/stats/library-usage
|
|
auth: jwt
|
|
area: stats
|
|
---
|
|
<!-- source: internal/handlers/stats.go:88 StatsHandler.LibraryUsage -> services/statistics_service.go:586 LibraryUsageStats -->
|
|
|
|
# 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
|
|
|
|
```http
|
|
GET /api/stats/library-usage?library_id=8c8d0cee-1cca-4def-9a37-edad3a4d438b
|
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
```
|
|
|
|
## Response (200 OK)
|
|
|
|
```json example-response
|
|
{
|
|
"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 |
|