diff --git a/docs/developer/api/media-items/download_media_item_file.md b/docs/developer/api/media-items/download_media_item_file.md new file mode 100644 index 00000000..67ecaf30 --- /dev/null +++ b/docs/developer/api/media-items/download_media_item_file.md @@ -0,0 +1,26 @@ +--- +method: GET +path: /api/media-items/:id/download +auth: jwt +area: media-items +--- + + +# 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 | diff --git a/docs/developer/api/media-items/filter_count.md b/docs/developer/api/media-items/filter_count.md new file mode 100644 index 00000000..873cc9b5 --- /dev/null +++ b/docs/developer/api/media-items/filter_count.md @@ -0,0 +1,33 @@ +--- +method: GET +path: /api/media-items/filter-count +auth: jwt +area: media-items +--- + + +# 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 | diff --git a/docs/developer/api/media-items/get_facets.md b/docs/developer/api/media-items/get_facets.md new file mode 100644 index 00000000..9837e07a --- /dev/null +++ b/docs/developer/api/media-items/get_facets.md @@ -0,0 +1,48 @@ +--- +method: GET +path: /api/media-items/facets +auth: jwt +area: media-items +--- + + +# Get Media Item Facets + +Two modes, both returning small JSON objects: + +1. **Facet values** (primary): `?facet=&library_id=` — lists the + distinct values of a facet within one visible library. +2. **Autocomplete counts**: `?field=&library_id=` — 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 | diff --git a/docs/developer/api/media-items/merge_archived_media_item.md b/docs/developer/api/media-items/merge_archived_media_item.md new file mode 100644 index 00000000..f0c65395 --- /dev/null +++ b/docs/developer/api/media-items/merge_archived_media_item.md @@ -0,0 +1,40 @@ +--- +method: POST +path: /api/media-items/:id/merge +auth: jwt+admin +area: media-items +--- + + +# 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 | diff --git a/docs/developer/api/media-items/purge_archived_media_items.md b/docs/developer/api/media-items/purge_archived_media_items.md new file mode 100644 index 00000000..bfcd67f6 --- /dev/null +++ b/docs/developer/api/media-items/purge_archived_media_items.md @@ -0,0 +1,29 @@ +--- +method: POST +path: /api/media-items/purge-archived +auth: jwt+admin +area: media-items +--- + + +# 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 | diff --git a/docs/developer/api/media-items/rescan_media_item.md b/docs/developer/api/media-items/rescan_media_item.md new file mode 100644 index 00000000..f42a45bc --- /dev/null +++ b/docs/developer/api/media-items/rescan_media_item.md @@ -0,0 +1,43 @@ +--- +method: POST +path: /api/media-items/:id/rescan +auth: jwt+admin +area: media-items +--- + + +# 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 | diff --git a/docs/developer/api/media-items/set_audio_speed.md b/docs/developer/api/media-items/set_audio_speed.md new file mode 100644 index 00000000..d353ceea --- /dev/null +++ b/docs/developer/api/media-items/set_audio_speed.md @@ -0,0 +1,37 @@ +--- +method: PUT +path: /api/media-items/:id/audio-speed +auth: jwt +area: media-items +--- + + +# 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 | diff --git a/docs/developer/api/media-items/unarchive_media_item.md b/docs/developer/api/media-items/unarchive_media_item.md new file mode 100644 index 00000000..86aaebbc --- /dev/null +++ b/docs/developer/api/media-items/unarchive_media_item.md @@ -0,0 +1,20 @@ +--- +method: POST +path: /api/media-items/:id/unarchive +auth: jwt+admin +area: media-items +--- + + +# 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 | diff --git a/internal/docs/schema.go b/internal/docs/schema.go index 00e91d47..1e75d3c7 100644 --- a/internal/docs/schema.go +++ b/internal/docs/schema.go @@ -207,4 +207,35 @@ var Schemas = map[string]Schema{ Request: nil, // kind via required ?annotation_type= 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}`), + }, }