diff --git a/docs/developer/api/admin/hash_conflicts_api.md b/docs/developer/api/admin/hash_conflicts_api.md new file mode 100644 index 00000000..360d88a4 --- /dev/null +++ b/docs/developer/api/admin/hash_conflicts_api.md @@ -0,0 +1,52 @@ +--- +method: GET +path: /api/admin/hash-conflicts +auth: jwt+admin +area: admin +--- + + +# Hash Conflicts (API) + +Lists pending content-duplicate groups (same library + SHA-256 at different +paths) with per-item usage counts. + +## Example response + +```json example-response +{ + "conflicts": [ + { + "id": "uuid", + "library_id": "uuid", + "library_name": "Manga", + "sha256": "abc123...", + "created_at": "2026-10-24T13:57:01Z", + "items": [ + { + "id": "uuid", + "title": "Book Title", + "author": "Author", + "file_path": "/path/to/book.epub", + "file_size": 12345, + "created_at": "2026-10-24T13:57:01Z", + "progress_count": 0, + "highlight_count": 0, + "bookmark_count": 0, + "note_count": 0, + "collection_count": 0 + } + ] + } + ], + "total": 1 +} +``` + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 401 | `{"error": "unauthorized"}` | no/invalid JWT | +| 403 | `{"error": "admin access required"}` | non-admin | +| 500 | `{"error": "failed to list hash conflicts"}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/collections/bulk_add_books.md b/docs/developer/api/collections/bulk_add_books.md new file mode 100644 index 00000000..76ce0304 --- /dev/null +++ b/docs/developer/api/collections/bulk_add_books.md @@ -0,0 +1,51 @@ +--- +method: POST +path: /api/collections/bulk-add-books +auth: jwt +area: collections +--- + + +# Bulk Add Books + +Adds books to multiple collections in one request. + +## Example request + +```json example-request +{ + "operations": [ + { + "collection_id": "550e8400-e29b-41d4-a716-446655440000", + "book_ids": [ + "660e8400-e29b-41d4-a716-446655440001" + ] + } + ] +} +``` + +## Example response + +```json example-response +{ + "results": [], + "total": 1 +} +``` + +## Fields + +| Field | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| operations | array | Yes | List of {collection_id, book_ids} | +| operations[].collection_id | string (uuid) | Yes | Target collection | +| operations[].book_ids | array of string (uuid) | Yes | Books to add | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "Invalid request"}` / `{"error": "operations required"}` | malformed | +| 400 | `{"error": "smart collections are rule-based..."}` | smart collection | +| 500 | `{"error": "..."}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/collections/bulk_remove_books.md b/docs/developer/api/collections/bulk_remove_books.md new file mode 100644 index 00000000..3a90aa6f --- /dev/null +++ b/docs/developer/api/collections/bulk_remove_books.md @@ -0,0 +1,45 @@ +--- +method: POST +path: /api/collections/:id/books/bulk-remove +auth: jwt +area: collections +--- + + +# Bulk Remove Books + +Removes multiple books from a collection at once. + +## Example request + +```json example-request +{ + "book_ids": [ + "550e8400-e29b-41d4-a716-446655440000", + "660e8400-e29b-41d4-a716-446655440001" + ] +} +``` + +## Example response + +```json example-response +{ + "removed": 2, + "total": 2 +} +``` + +## Fields + +| Field | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| book_ids | array of string (uuid) | Yes | Book ids to remove | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "invalid collection id"}` / `{"error": "invalid request"}` | malformed | +| 400 | `{"error": "smart collections are rule-based..."}` | smart collection | +| 500 | `{"error": "..."}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/collections/delete_shelf_mapping.md b/docs/developer/api/collections/delete_shelf_mapping.md index 8c850710..2588d27b 100644 --- a/docs/developer/api/collections/delete_shelf_mapping.md +++ b/docs/developer/api/collections/delete_shelf_mapping.md @@ -1,3 +1,11 @@ +--- +method: DELETE +path: /api/devices/:id/collections/:collectionId +auth: jwt +area: collections +--- + + # Delete Shelf Mapping Remove a collection-to-shelf mapping for a device. diff --git a/docs/developer/api/collections/get_collection_books.md b/docs/developer/api/collections/get_collection_books.md new file mode 100644 index 00000000..b7d07f47 --- /dev/null +++ b/docs/developer/api/collections/get_collection_books.md @@ -0,0 +1,36 @@ +--- +method: GET +path: /api/collections/:id/books +auth: jwt +area: collections +--- + + +# Get Book Collections + +Returns the collections a given book belongs to. + +## Example response + +```json example-response +{ + "book_id": "550e8400-e29b-41d4-a716-446655440000", + "collections": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "To Read", + "description": "Books I want to read soon", + "color": "#00FF00", + "icon": "📖" + } + ], + "total": 1 +} +``` + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "invalid book id"}` | malformed id | +| 500 | `{"error": "..."}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/collections/preview_collection.md b/docs/developer/api/collections/preview_collection.md new file mode 100644 index 00000000..027ec496 --- /dev/null +++ b/docs/developer/api/collections/preview_collection.md @@ -0,0 +1,46 @@ +--- +method: POST +path: /api/collections/preview +auth: jwt +area: collections +--- + + +# Preview Collection + +Previews which books would match a set of rules without saving. + +## Example request + +```json example-request +{ + "library_id": "550e8400-e29b-41d4-a716-446655440000", + "rules": [], + "manual_book_ids": [], + "limit": 20 +} +``` + +## Example response + +```json example-response +{ + "items": [] +} +``` + +## Fields + +| Field | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| library_id | string (uuid) | Yes | Library to preview against | +| rules | array | No | Rule objects | +| manual_book_ids | array of string (uuid) | No | Manually included books | +| limit | int | No | Max items (default 20) | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "Invalid request"}` / `{"error": "invalid library_id"}` / `{"error": "library not found"}` | malformed | +| 500 | `{"error": "..."}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/koreader/get_rating.md b/docs/developer/api/koreader/get_rating.md new file mode 100644 index 00000000..7f753d8a --- /dev/null +++ b/docs/developer/api/koreader/get_rating.md @@ -0,0 +1,21 @@ +--- +method: GET +path: /api/sync/koreader/rating +auth: device-token +area: koreader +--- + + +# Get KOReader Rating + +Returns the stored rating for a book (koreader device-token auth). + +## Example response + +```json example-response +{ + "rating": 8 +} +``` + +When unrated, `rating` is `null`. \ No newline at end of file diff --git a/docs/developer/api/koreader/get_review.md b/docs/developer/api/koreader/get_review.md new file mode 100644 index 00000000..f8f2ef02 --- /dev/null +++ b/docs/developer/api/koreader/get_review.md @@ -0,0 +1,21 @@ +--- +method: GET +path: /api/sync/koreader/review +auth: device-token +area: koreader +--- + + +# Get KOReader Review + +Returns the raw markdown review for a book. + +## Example response + +```json example-response +{ + "review": "A slow start, but the second act earns it." +} +``` + +When no review exists, `review` is `null`. \ No newline at end of file diff --git a/docs/developer/api/koreader/push_stats.md b/docs/developer/api/koreader/push_stats.md new file mode 100644 index 00000000..24716c38 --- /dev/null +++ b/docs/developer/api/koreader/push_stats.md @@ -0,0 +1,34 @@ +--- +method: POST +path: /api/sync/koreader/stats +auth: device-token +area: koreader +--- + + +# Push KOReader Stats + +Pushes cumulative reading seconds for a book; only the delta above the +stored watermark becomes a reading_history session. + +## Example request + +```json example-request +{ + "total_seconds": 3600 +} +``` + +## Example response + +```json example-response +{ + "total_seconds": 3600, + "watermark": 0, + "delta_seconds": 3600, + "session_recorded": true, + "session_seconds": 3600 +} +``` + +UNVERIFIED: the request body also carries the koreader book reference fields (md5, title, etc.) used to resolve the book; field set not copied here. \ No newline at end of file diff --git a/docs/developer/api/koreader/set_rating.md b/docs/developer/api/koreader/set_rating.md new file mode 100644 index 00000000..46f36164 --- /dev/null +++ b/docs/developer/api/koreader/set_rating.md @@ -0,0 +1,29 @@ +--- +method: POST +path: /api/sync/koreader/rating +auth: device-token +area: koreader +--- + + +# Set KOReader Rating + +Sets (or clears, when rating is 0) the rating for a book. + +## Example request + +```json example-request +{ + "rating": 8 +} +``` + +## Example response + +```json example-response +{ + "rating": 8 +} +``` + +A rating of 0 clears the stored rating and returns `{"rating": null}`. \ No newline at end of file diff --git a/docs/developer/api/koreader/set_review.md b/docs/developer/api/koreader/set_review.md new file mode 100644 index 00000000..fcca6d52 --- /dev/null +++ b/docs/developer/api/koreader/set_review.md @@ -0,0 +1,29 @@ +--- +method: POST +path: /api/sync/koreader/review +auth: device-token +area: koreader +--- + + +# Set KOReader Review + +Sets (or clears, when empty) the review for a book. + +## Example request + +```json example-request +{ + "review": "A slow start, but the second act earns it." +} +``` + +## Example response + +```json example-response +{ + "review": "A slow start, but the second act earns it." +} +``` + +An empty review clears the stored review and returns `{"review": null}`. \ No newline at end of file diff --git a/docs/developer/api/media-items/bulk_delete_media_items.md b/docs/developer/api/media-items/bulk_delete_media_items.md index 1f899553..b880aa74 100644 --- a/docs/developer/api/media-items/bulk_delete_media_items.md +++ b/docs/developer/api/media-items/bulk_delete_media_items.md @@ -1,3 +1,11 @@ +--- +method: POST +path: /api/media-items/bulk-delete +auth: jwt +area: media-items +--- + + # Bulk Delete Media Items Delete multiple media items at once (supports ebooks, comics, manga). @@ -14,7 +22,7 @@ Delete multiple media items at once (supports ebooks, comics, manga). ### Example Request -```json +```json example-request { "media_item_ids": [ "550e8400-e29b-41d4-a716-446655440001", @@ -26,7 +34,7 @@ Delete multiple media items at once (supports ebooks, comics, manga). ## Response (200 OK) -```json +```json example-response { "results": [ { diff --git a/docs/developer/api/media-items/bulk_update_media_items.md b/docs/developer/api/media-items/bulk_update_media_items.md index 58a23b0c..87b4ea50 100644 --- a/docs/developer/api/media-items/bulk_update_media_items.md +++ b/docs/developer/api/media-items/bulk_update_media_items.md @@ -1,3 +1,11 @@ +--- +method: POST +path: /api/media-items/bulk-update +auth: jwt +area: media-items +--- + + # Bulk Update Media Items Update multiple media items at once (supports ebooks, comics, manga). @@ -26,7 +34,7 @@ Update multiple media items at once (supports ebooks, comics, manga). ### Example Request -```json +```json example-request { "media_item_updates": [ { @@ -50,7 +58,7 @@ Update multiple media items at once (supports ebooks, comics, manga). ## Response (200 OK) -```json +```json example-response { "results": [ { diff --git a/docs/developer/api/media-items/deleted_annotations.md b/docs/developer/api/media-items/deleted_annotations.md index 3812fec9..76b77fe9 100644 --- a/docs/developer/api/media-items/deleted_annotations.md +++ b/docs/developer/api/media-items/deleted_annotations.md @@ -1,3 +1,11 @@ +--- +method: GET +path: /api/media-items/:id/annotations/deleted +auth: jwt +area: media-items +--- + + # Deleted Annotations History List, restore, or permanently delete tombstoned annotations (highlights, @@ -18,7 +26,7 @@ Returns tombstoned annotations for the book, newest deletion first. ### Response (200 OK) -```json +```json example-response { "deleted_annotations": [ { diff --git a/docs/developer/api/media-items/get_audio_chapters.md b/docs/developer/api/media-items/get_audio_chapters.md index 1e62cddb..2edbb1b8 100644 --- a/docs/developer/api/media-items/get_audio_chapters.md +++ b/docs/developer/api/media-items/get_audio_chapters.md @@ -1,3 +1,11 @@ +--- +method: GET +path: /api/media-items/:id/chapters +auth: jwt +area: media-items +--- + + # Get Audio Chapters The format-abstraction chapter list for audiobooks, sourced from @@ -11,7 +19,7 @@ which file model produced them. ## Response -```json +```json example-response { "media_item_id": "…", "chapters": [ diff --git a/docs/developer/api/media-items/get_audio_track.md b/docs/developer/api/media-items/get_audio_track.md index 8a18b7f5..a4aa096d 100644 --- a/docs/developer/api/media-items/get_audio_track.md +++ b/docs/developer/api/media-items/get_audio_track.md @@ -1,3 +1,11 @@ +--- +method: GET +path: /api/media-items/:id/audio/:trackIndex +auth: jwt +area: media-items +--- + + # Get Audio Track Streams one playable track of a multi-file audiobook (folder-of-tracks diff --git a/internal/docs/schema.go b/internal/docs/schema.go index 7e85bfb6..d293132c 100644 --- a/internal/docs/schema.go +++ b/internal/docs/schema.go @@ -860,4 +860,101 @@ var Schemas = map[string]Schema{ "device_shelf_name": "", "sync_direction": "", "created_at": "" }`), }, + + // --- Beam batch 11: final backfill --- + "GET /api/collections/:id/books": { + // Reverse listing: which collections carry this book. + Response: json.RawMessage(`{ + "book_id": "", "collections": [], "total": 0 + }`), + }, + "POST /api/collections/:id/books/bulk-remove": { + Request: handlers.BulkRemoveBooksRequest{}, + Response: json.RawMessage(`{"removed": 0, "total": 0}`), + }, + "POST /api/collections/bulk-add-books": { + Request: json.RawMessage(`{ + "operations": [{"collection_id": "", "book_ids": []}] + }`), + Response: json.RawMessage(`{"results": [], "total": 0}`), + }, + "POST /api/collections/preview": { + // Smart-collection dry run — inline request struct. + Request: json.RawMessage(`{ + "library_id": "", "rules": [], "manual_book_ids": [], "limit": 0 + }`), + Response: json.RawMessage(`{"items": []}`), + }, + "DELETE /api/devices/:id/collections/:collectionId": { + // 204 No Content. + }, + "POST /api/media-items/bulk-delete": { + Request: json.RawMessage(`{"media_item_ids": []}`), + Response: json.RawMessage(`{ + "results": [], "total": 0, "deleted": 0, "failed": 0 + }`), + }, + "POST /api/media-items/bulk-update": { + // updates fields are pointers (partial per-item patch). + Request: json.RawMessage(`{ + "media_item_updates": [ + { + "media_item_id": "", + "updates": { + "title": "", "author": "", "genre": "", + "language": "", "tags": [] + } + } + ] + }`), + Response: json.RawMessage(`{ + "results": [], "total": 0, "updated": 0, "failed": 0 + }`), + }, + "GET /api/media-items/:id/annotations/deleted": { + Response: json.RawMessage(`{ + "deleted_annotations": [], "total": 0 + }`), + }, + "GET /api/media-items/:id/chapters": { + Response: json.RawMessage(`{ + "media_item_id": "", "chapters": [], "duration_seconds": 0, + "codec_profile": {}, "layout": "", "tracks": [] + }`), + }, + "GET /api/media-items/:id/audio/:trackIndex": { + // Raw audio bytes via http.ServeContent (Range support) — not JSON. + }, + "GET /api/sync/koreader/rating": { + // Book identity via query params (uuid/sha256/title/authors). + Response: json.RawMessage(`{"rating": null}`), + }, + "POST /api/sync/koreader/rating": { + // koreaderBookRef embedded + rating 0-10 (odd = half-star). + Request: json.RawMessage(`{ + "uuid": "", "sha256": "", "title": "", "authors": [], "rating": 0 + }`), + Response: json.RawMessage(`{"rating": null}`), + }, + "GET /api/sync/koreader/review": { + Response: json.RawMessage(`{"review": null}`), + }, + "POST /api/sync/koreader/review": { + Request: json.RawMessage(`{ + "uuid": "", "sha256": "", "title": "", "authors": [], "review": "" + }`), + Response: json.RawMessage(`{"review": null}`), + }, + "POST /api/sync/koreader/stats": { + Request: json.RawMessage(`{ + "uuid": "", "sha256": "", "title": "", "authors": [], + "total_seconds": 0 + }`), + Response: AnyJSONObject{}, // hand-built result map + }, + "GET /api/admin/hash-conflicts": { + // JSON list endpoint (admin). The resolve POST is an htmx fragment + // renderer — deliberately NOT documented (rule 3). + Response: json.RawMessage(`{"conflicts": [], "total": 0}`), + }, }