docs(media-items): Work Order 3 batch 3b — management endpoints (8)
New frontmatter'd docs:
- download (ServeFile — raw bytes, family-lens enforced; corrects the old
download doc's claim that no download route is registered: ServeFile IS
routed, DownloadBook is the dead code)
- set_audio_speed (speed range 0.25-4.0, NaN/Inf rejected)
- rescan (abridged example over the full 70+-column row; write-after-success
guarantee noted)
- purge-archived (destructive, admin), unarchive (204), merge (SHA-256
precondition, per-user collisions keep active copy)
- facets (two modes: facet+values, field autocomplete counts)
- filter_count (family lens)
Registry: RawMessage pins for map/abridged responses; Schema{} for non-JSON
body routes (download = raw bytes; unarchive = 204). Audit now:
missing docs down to ~55 (from 112), 0 orphans.
This commit is contained in:
1 parent
ed0ce5d077
commit
4a6fb033cb
9 files changed
+307
No files matched your search
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
method: GET
|
||||||
|
path: /api/media-items/:id/download
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:2766 MediaHandler.ServeFile -->
|
||||||
|
|
||||||
|
# Download Media Item File
|
||||||
|
|
||||||
|
Serves the book file itself (epub, pdf, …) for the given media item, enforced
|
||||||
|
against the requester's **visible-library set** (family lens). This is the
|
||||||
|
registered route — distinct from the dead `MediaHandler.DownloadBook` code
|
||||||
|
path referenced by pre-2026 docs.
|
||||||
|
|
||||||
|
Success is the raw file bytes with the file's MIME type. There is no JSON
|
||||||
|
body.
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "invalid library ID"}` / `{"error": "invalid path"}` / `{"error": "invalid media item ID"}` | malformed ids |
|
||||||
|
| 403 | `{"error": "library not accessible"}` | library outside the visible set |
|
||||||
|
| 404 | `{"error": "book not found"}` / `{"error": "book file not found"}` / `{"error": "file not found"}` | missing row or file on disk |
|
||||||
|
| 500 | `{"error": "failed to check library access"}` | unexpected server error |
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
method: GET
|
||||||
|
path: /api/media-items/filter-count
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:2713 MediaHandler.FilterCount -->
|
||||||
|
|
||||||
|
# Filter Count
|
||||||
|
|
||||||
|
Returns how many media items the current filters match, scoped to the
|
||||||
|
requester's visible libraries. Used by the filter UI's live count.
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"count": 42
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Query parameters
|
||||||
|
|
||||||
|
UNVERIFIED: filter parameters mirror the media-items list endpoint (same
|
||||||
|
filter machinery); pass them through unchanged.
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "..."}` | invalid filter params |
|
||||||
|
| 403 | `{"error": "library not visible"}` | family lens |
|
||||||
|
| 500 | `{"error": "..."}` | unexpected server error |
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
method: GET
|
||||||
|
path: /api/media-items/facets
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:2532 MediaHandler.GetFacets -->
|
||||||
|
|
||||||
|
# Get Media Item Facets
|
||||||
|
|
||||||
|
Two modes, both returning small JSON objects:
|
||||||
|
|
||||||
|
1. **Facet values** (primary): `?facet=<param>&library_id=<uuid>` — lists the
|
||||||
|
distinct values of a facet within one visible library.
|
||||||
|
2. **Autocomplete counts**: `?field=<key>&library_id=<uuid>` — returns
|
||||||
|
`{"facets": {...}}` value counts for dropdowns.
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"facet": "author",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "George Orwell"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
UNVERIFIED: exact element shape of `values` (service-returned rows).
|
||||||
|
|
||||||
|
## Query parameters
|
||||||
|
|
||||||
|
| Param | Type | Notes |
|
||||||
|
| ----- | ---- | ----- |
|
||||||
|
| facet | string | facet param name; requires `library_id` |
|
||||||
|
| field | string | registry field key (autocomplete mode) |
|
||||||
|
| library_id | uuid | required for facet mode; must be visible to the requester |
|
||||||
|
| limit | number | caps facet values |
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "library_id is required"}` / `{"error": "unknown facet: ..."}` / `{"error": "facet does not list values: ..."}` / `{"error": "invalid library_id"}` | bad params |
|
||||||
|
| 403 | `{"error": "library not visible"}` | family lens |
|
||||||
|
| 500 | `{"error": "..."}` | unexpected server error |
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
method: POST
|
||||||
|
path: /api/media-items/:id/merge
|
||||||
|
auth: jwt+admin
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:1674 MediaHandler.MergeArchivedMediaItem -->
|
||||||
|
|
||||||
|
# Merge Archived Media Item
|
||||||
|
|
||||||
|
**Admin only.** Merges an archived/missing item's reading data (progress,
|
||||||
|
annotations, highlights, notes, bookmarks, ratings, collections, Kobo
|
||||||
|
shelves) into an active item with identical content (same SHA-256), then
|
||||||
|
deletes the archived row. Covers the copy-then-delete-later workflow. Per-user
|
||||||
|
collisions keep the active copy's data (mirrors hash-conflict resolution).
|
||||||
|
|
||||||
|
## Example request
|
||||||
|
|
||||||
|
```json example-request
|
||||||
|
{
|
||||||
|
"target_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"message": "Merged reading progress, annotations, and collections into the active item"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "source item is not archived or missing"}` / `{"error": "target item is not active"}` / `{"error": "items do not share the same content hash"}` | precondition violations |
|
||||||
|
| 403 | `{"error": "admin access required"}` | not an admin |
|
||||||
|
| 404 | `{"error": "archived item not found"}` / `{"error": "target item not found"}` | missing rows |
|
||||||
|
| 500 | `{"error": "failed to merge reading data: ..."}` | unexpected server error |
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
method: POST
|
||||||
|
path: /api/media-items/purge-archived
|
||||||
|
auth: jwt+admin
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:1747 MediaHandler.PurgeArchivedMediaItems -->
|
||||||
|
|
||||||
|
# Purge Archived Media Items
|
||||||
|
|
||||||
|
**Admin only.** Permanently deletes ALL archived media items and their
|
||||||
|
cover files. This is destructive — archived rows hold no reading data merge
|
||||||
|
path (use [Merge Archived Media Item](merge_archived_media_item.md) per item
|
||||||
|
first if reading history should survive).
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"purged": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 403 | `{"error": "admin access required"}` | not an admin |
|
||||||
|
| 500 | `{"error": "..."}` | unexpected server error |
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
method: POST
|
||||||
|
path: /api/media-items/:id/rescan
|
||||||
|
auth: jwt+admin
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:1765 MediaHandler.RescanMediaItem -->
|
||||||
|
|
||||||
|
# Rescan Media Item
|
||||||
|
|
||||||
|
Re-extracts metadata for one media item from its file and returns the updated
|
||||||
|
row (200, full `media_items` record — 70+ columns; the most relevant are shown
|
||||||
|
below).
|
||||||
|
|
||||||
|
Write-after-success guarantee: a failed/empty extraction never clears
|
||||||
|
scan-derived columns.
|
||||||
|
|
||||||
|
## Example response (abridged)
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"library_id": "990e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"title": "1984",
|
||||||
|
"author": "George Orwell",
|
||||||
|
"file_path": "/books/1984.epub",
|
||||||
|
"mime_type": "application/epub+zip",
|
||||||
|
"scan_status": "completed",
|
||||||
|
"updated_at": "2026-10-24T14:57:01Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The example is abridged for readability — the response carries the full
|
||||||
|
`media_items` row.
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "invalid media item id"}` | malformed id |
|
||||||
|
| 403 | `{"error": "admin access required"}` | not an admin |
|
||||||
|
| 404 | `{"error": "media item not found"}` | no such item |
|
||||||
|
| 500 | `{"error": "..."}` | extraction/DB failure |
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
method: PUT
|
||||||
|
path: /api/media-items/:id/audio-speed
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:3033 MediaHandler.SetAudioSpeed -->
|
||||||
|
|
||||||
|
# Set Audio Speed
|
||||||
|
|
||||||
|
Persists the audiobook playback speed for the signed-in user on one media
|
||||||
|
item. Speed must be within **0.25–4.0** (NaN/Inf rejected).
|
||||||
|
|
||||||
|
## Example request
|
||||||
|
|
||||||
|
```json example-request
|
||||||
|
{
|
||||||
|
"speed": 1.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"playback_speed": 1.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "invalid media item id"}` / `{"error": "invalid request"}` / `{"error": "speed out of range"}` | malformed or out-of-range |
|
||||||
|
| 401 | `{"error": "authentication required"}` / `{"error": "invalid user"}` | no/invalid auth |
|
||||||
|
| 404 | `{"error": "book not found"}` | no such media item |
|
||||||
|
| 500 | `{"error": "failed to save playback speed"}` | unexpected server error |
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
method: POST
|
||||||
|
path: /api/media-items/:id/unarchive
|
||||||
|
auth: jwt+admin
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:1646 MediaHandler.UnarchiveMediaItem -->
|
||||||
|
|
||||||
|
# Unarchive Media Item
|
||||||
|
|
||||||
|
**Admin only.** Clears the archive flag from a media item so it re-enters the
|
||||||
|
active library. Success is **204 No Content** — no body.
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "invalid media item id"}` | malformed id |
|
||||||
|
| 403 | `{"error": "admin access required"}` | not an admin |
|
||||||
|
| 500 | `{"error": "..."}` | unexpected server error |
|
||||||
@@ -207,4 +207,35 @@ var Schemas = map[string]Schema{
|
|||||||
Request: nil, // kind via required ?annotation_type=
|
Request: nil, // kind via required ?annotation_type=
|
||||||
Response: json.RawMessage(`{"purged": false}`),
|
Response: json.RawMessage(`{"purged": false}`),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// --- media-items batch 3b: management ---
|
||||||
|
"GET /api/media-items/:id/download": {
|
||||||
|
// Serves raw file bytes, not JSON — no examples by design.
|
||||||
|
},
|
||||||
|
"PUT /api/media-items/:id/audio-speed": {
|
||||||
|
Request: json.RawMessage(`{"speed": 0}`),
|
||||||
|
Response: json.RawMessage(`{"playback_speed": 0}`),
|
||||||
|
},
|
||||||
|
"POST /api/media-items/:id/rescan": {
|
||||||
|
// Returns the FULL media_items row (70+ columns); pin is an abridged
|
||||||
|
// key subset — the doc example declares the same subset and says so.
|
||||||
|
Response: json.RawMessage(`{
|
||||||
|
"id": "", "library_id": "", "title": "", "author": "",
|
||||||
|
"file_path": "", "mime_type": "", "scan_status": "", "updated_at": ""
|
||||||
|
}`),
|
||||||
|
},
|
||||||
|
"POST /api/media-items/purge-archived": {
|
||||||
|
Response: json.RawMessage(`{"purged": 0}`),
|
||||||
|
},
|
||||||
|
"POST /api/media-items/:id/unarchive": {},
|
||||||
|
"POST /api/media-items/:id/merge": {
|
||||||
|
Request: json.RawMessage(`{"target_id": ""}`),
|
||||||
|
Response: json.RawMessage(`{"message": ""}`),
|
||||||
|
},
|
||||||
|
"GET /api/media-items/facets": {
|
||||||
|
Response: json.RawMessage(`{"facet": "", "values": [{}]}`),
|
||||||
|
},
|
||||||
|
"GET /api/media-items/filter-count": {
|
||||||
|
Response: json.RawMessage(`{"count": 0}`),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user