Files
bookhoard/docs/developer/api/stats/get_library_usage.md
T
John O'Keefe 6fc501b35a docs(stats): Claude-side pinning — all 4 stats endpoints + UNVERIFIED sweep to zero
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.
2026-10-24 18:25:12 -04:00

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