diff --git a/bruno/collections/Get Book Collections.yml b/bruno/collections/Get Book Collections.yml index d09b21de..0c879284 100644 --- a/bruno/collections/Get Book Collections.yml +++ b/bruno/collections/Get Book Collections.yml @@ -5,7 +5,7 @@ info: http: method: GET - url: "{{base_url}}/api/collections/books" + url: "{{base_url}}/api/collections/{{collection_id}}/books" body: type: json data: |- @@ -27,7 +27,7 @@ docs: |- **Method:** GET - **Endpoint:** /api/collections/books + **Endpoint:** /api/collections/{{collection_id}}/books **Authentication:** Required (Bearer token) diff --git a/bruno/library/Get User Visible Libraries.yml b/bruno/library/Get User Visible Libraries.yml index 6d493e88..5a342587 100644 --- a/bruno/library/Get User Visible Libraries.yml +++ b/bruno/library/Get User Visible Libraries.yml @@ -5,7 +5,7 @@ info: http: method: GET - url: "{{base_url}}/api/libraries/visible" + url: "{{base_url}}/api/libraries/visibility" headers: - name: "" value: application/json @@ -24,7 +24,7 @@ docs: |- **Method:** GET - **Endpoint:** /api/libraries/visible + **Endpoint:** /api/libraries/visibility **Authentication:** Required (Bearer token) diff --git a/bruno/media-items/Get Audio Track.yml b/bruno/media-items/Get Audio Track.yml index 0e4f608e..2d47c859 100644 --- a/bruno/media-items/Get Audio Track.yml +++ b/bruno/media-items/Get Audio Track.yml @@ -5,7 +5,7 @@ info: http: method: GET - url: "{{base_url}}/api/media-items/{{media_item_id}}/audio/0" + url: "{{base_url}}/api/media-items/{{media_item_id}}/audio/{{track_index}}" headers: - name: Range value: bytes=0-1023 diff --git a/docs/developer/api-reference.md b/docs/developer/api-reference.md deleted file mode 100644 index 86e5262a..00000000 --- a/docs/developer/api-reference.md +++ /dev/null @@ -1,1754 +0,0 @@ -# Bookhoard API Reference - -> **⚠️ Legacy Document**: This is a monolithic API reference (1,600+ lines). -> For updated, split endpoint documentation with interactive API explorer, see [API Documentation Portal](api/api-reference.md). -> -> **Use the split docs for:** -> -> - Easier navigation by category -> - Interactive API explorer -> - Endpoint-specific examples -> - Latest updates - ---- - -Complete API documentation for Bookhoard v1.0 with Universal Cross-Platform Sync support. - -## Table of Contents - -1. [Authentication](#authentication) -2. [Users & Profiles](#users--profiles) -3. [Libraries](#libraries) -4. [Media Items](#media-items) -5. [Reading Progress](#reading-progress) -6. [Notes & Highlights](#notes--highlights) -7. [Ratings](#ratings) -8. [Device Management](#device-management) -9. [Analytics](#analytics) -10. [Book Matching & Linking](#book-matching--linking) -11. [Collections](#collections) → See [Collections API](collections-api.md) -12. [OPDS](#opds-open-publication-distribution-system) -13. [Sync Protocol - KOReader](#sync-protocol---koreader) -14. [Sync Protocol - Kobo](#sync-protocol---kobo) -15. [Universal Progress](#universal-progress) -16. [Conflicts](#conflicts) -17. [Sync Queue](#sync-queue) -18. [System Settings & Configuration](#system-settings--configuration) -19. [Hash Conflicts](#hash-conflicts) -20. [WebSocket](#websocket) - -## Base URL - -``` -Production: https://your-domain.com/api -Development: http://localhost:8765/api -``` - -## Authentication - -Most endpoints require authentication. Include your JWT token in the Authorization header: - -``` -Authorization: Bearer -``` - -### Register User - -```http -POST /api/auth/register -Content-Type: application/json - -{ - "email": "user@example.com", - "username": "john", - "password": "SecureP@ss123!", - "first_name": "John", - "last_name": "Doe" -} -``` - -**Response** (201): - -```json -{ - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "refresh_token": "d4f5g6h7...", - "user": { - "id": "uuid-here", - "email": "user@example.com", - "username": "john", - "role": "user", - "theme": "tokyo-night", - "created_at": "2026-01-31T10:00:00Z" - } -} -``` - -### Login - -```http -POST /api/auth/login -Content-Type: application/json - -{ - "email": "user@example.com", - "password": "SecureP@ss123!" -} -``` - -**Response** (200): - -```json -{ - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "refresh_token": "d4f5g6h7...", - "user": { - "id": "uuid-here", - "email": "user@example.com", - "username": "john", - "role": "user" - } -} -``` - -### Refresh Token - -```http -POST /api/auth/refresh -Content-Type: application/json - -{ - "refresh_token": "d4f5g6h7..." -} -``` - -**Response** (200): - -```json -{ - "token": "new-jwt-token", - "refresh_token": "new-refresh-token" -} -``` - -### Logout - -```http -POST /api/auth/logout -Authorization: Bearer -``` - -**Response** (204): No Content - -## Users & Profiles - -### Get Current User - -```http -GET /api/users/me -Authorization: Bearer -``` - -**Response** (200): - -```json -{ - "id": "uuid", - "email": "user@example.com", - "username": "john", - "first_name": "John", - "last_name": "Doe", - "theme": "tokyo-night", - "role": "user", - "max_devices": 10, - "created_at": "2026-01-31T10:00:00Z" -} -``` - -### Update Profile - -```http -PUT /api/users/me/profile -Authorization: Bearer -Content-Type: application/json - -{ - "first_name": "John", - "last_name": "Smith" -} -``` - -### Update Theme - -```http -PUT /api/users/me/theme -Authorization: Bearer -Content-Type: application/json - -{ - "theme": "dracula" -} -``` - -### Change Password - -```http -PUT /api/users/me/password -Authorization: Bearer -Content-Type: application/json - -{ - "current_password": "oldPassword", - "new_password": "NewSecureP@ss123!" -} -``` - -### Update Scan Settings (Legacy) - -> Superseded by `PUT /api/system/settings` (see [System Settings & Configuration](#system-settings--configuration)); kept for backward compatibility. - -```http -PUT /api/libraries/scan-settings -Authorization: Bearer -Content-Type: application/json - -{ - "scan_poll_interval_seconds": 60, - "auto_scan_enabled": true -} -``` - -## Libraries - -### Get Visible Libraries - -```http -GET /api/libraries/visible -Authorization: Bearer -``` - -**Response** (200): - -```json -{ - "libraries": [ - { - "id": "uuid", - "name": "My Ebooks", - "description": "Ebook collection", - "type_name": "ebooks", - "is_visible": true - } - ] -} -``` - -### Get Library Details - -```http -GET /api/libraries/{library_id} -Authorization: Bearer -``` - -### Create Library (Admin Only) - -```http -POST /api/libraries -Authorization: Bearer -Content-Type: application/json - -{ - "name": "Comics Collection", - "description": "Digital comics", - "type": "comics" -} -``` - -### Add Library Folder (Admin Only) - -```http -POST /api/libraries/{library_id}/folders -Authorization: Bearer -Content-Type: application/json - -{ - "folder_path": "/path/to/comics" -} -``` - -### Set Library Visibility (Admin Only) - -```http -POST /api/libraries/visibility -Authorization: Bearer -Content-Type: application/json - -{ - "user_id": "user-uuid", - "library_id": "library-uuid", - "is_visible": true -} -``` - -## Media Items - -### List Media Items - -```http -GET /api/media-items?library_id={library_id}&limit=20&offset=0 -Authorization: Bearer -``` - -**Query Parameters**: - -- `library_id` (required): UUID of library -- `limit`: Number of items to return (max 100, default 20) -- `offset`: Number of items to skip - -**Response** (200): - -```json -{ - "media_items": [ - { - "id": "uuid", - "library_id": "uuid", - "title": "Book Title", - "author": "Author Name", - "description": "Book description", - "file_path": "/path/to/book.epub", - "file_size": 1024000, - "mime_type": "application/epub+zip", - "cover_image_path": "/path/to/cover.jpg", - "series": "Series Name", - "series_number": 1, - "tags": "sci-fi, space opera", - "language": "en", - "page_count": 350, - "genre": "Science Fiction", - "copyright_year": 2023, - "created_at": "2026-01-31T10:00:00Z" - } - ], - "total": 100 -} -``` - -### Get Media Item - -```http -GET /api/media-items/{media_id} -Authorization: Bearer -``` - -### Search Media Items - -```http -GET /api/media-items/search?q={query}&limit=20&offset=0 -Authorization: Bearer -``` - -**Query Parameters**: - -- `q` (required): Search query (minimum 2 characters) -- `limit`: Number of results (default 20) -- `offset`: Number to skip - -**Response** (200): - -```json -{ - "results": [ - { - "id": "uuid", - "title": "Book Title", - "author": "Author Name", - "match_score": 0.95 - } - ] -} -``` - -### Update Media Item (Admin Only) - -```http -PUT /api/media-items/{media_id} -Authorization: Bearer -Content-Type: application/json - -{ - "title": "Updated Title", - "author": "Updated Author", - "description": "Updated description", - "series": "Series", - "series_number": 2 -} -``` - -### Delete Media Item (Admin Only) - -```http -DELETE /api/media-items/{media_id} -Authorization: Bearer -``` - -## Reading Progress - -Full field reference: [api/progress/](api/progress/) — and read the -[Position Contract](api/progress/position-contract.md) (verification, -healing, the OPF spine numbering hazard, offset currencies) before -writing a client. - -### Get Reading Progress - -```http -GET /api/media-items/{media_id}/progress -Authorization: Bearer -``` - -**Response** (200): - -```json -{ - "id": "uuid", - "media_item_id": "uuid", - "user_id": "uuid", - "percentage": 0.045, - "epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/58/1:598)", - "context_text": "from day to day, but there was none in which ...", - "character_offset": 16375, - "current_page": null, - "total_pages": null, - "chapter": null, - "chapter_progress": null, - "format_group": "reflowable", - "total_characters": 592216, - "chapter_count": 1, - "last_read_at": "2026-09-26T14:52:34Z", - "last_sync_source": "koreader", - "css_selector": "body>div:nth-child(4)>p:nth-child(29)", - "anchor_href": "1984.xhtml", - "char_offset": 598 -} -``` - -`css_selector`, `anchor_href` and `char_offset` are server-derived -restore handles, served for convertible reflowable books with a -resolvable anchor. `char_offset` is UTF-16 code units within the anchor -block's text; `character_offset` is a book-wide rune count. Resolve the -document by `anchor_href`, never by the CFI's spine step (OPF numbering -includes `linear="no"` items). - -### Update Reading Progress - -```http -PUT /api/media-items/{media_id}/progress -Authorization: Bearer -Content-Type: application/json -``` - -```json -{ - "percentage": 0.0415, - "context_text": "was at war with one of these Powers it was generally a", - "epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/62/1:456)" -} -``` - -All fields are optional. The server verifies the submission against the -book and heals it on mismatch; the response is the stored row after -verification. A `percentage` below 0.005 is ignored while stored -progress exceeds 0.01 (first-page anti-clobber). - -### Delete Reading Progress - -```http -DELETE /api/media-items/{media_id}/progress -Authorization: Bearer -``` - -## Notes & Highlights - -### Get Notes - -```http -GET /api/media-items/{media_id}/notes -Authorization: Bearer -``` - -**Response** (200): - -```json -{ - "notes": [ - { - "id": "uuid", - "media_item_id": "uuid", - "user_id": "uuid", - "content": "This is an interesting passage...", - "position": "epubcfi(/6/4/2:15)", - "percentage_location": 0.45, - "character_start": 15432, - "character_end": 15480, - "epubcfi_location": "epubcfi(/6/4/2:15)", - "created_at": "2026-01-31T10:00:00Z", - "updated_at": "2026-01-31T10:00:00Z" - } - ] -} -``` - -### Create Note - -```http -POST /api/media-items/{media_id}/notes -Authorization: Bearer -Content-Type: application/json - -{ - "content": "This is a note", - "position": "epubcfi(/6/4/2:15)", - "percentage_location": 0.45, - "epubcfi_location": "epubcfi(/6/4/2:15)" -} -``` - -### Update Note - -```http -PUT /api/media-items/notes/{note_id} -Authorization: Bearer -Content-Type: application/json - -{ - "content": "Updated note content", - "position": "epubcfi(/6/4/2:20)" -} -``` - -### Delete Note - -```http -DELETE /api/media-items/notes/{note_id} -Authorization: Bearer -``` - -### Get Highlights - -```http -GET /api/media-items/{media_id}/highlights -Authorization: Bearer -``` - -**Response** (200): - -```json -{ - "highlights": [ - { - "id": "uuid", - "media_item_id": "uuid", - "user_id": "uuid", - "selection_text": "Highlighted text passage...", - "start_position": "epubcfi(/6/4/2:15)", - "end_position": "epubcfi(/6/4/2:20)", - "color": "#ffff00", - "percentage_start": 0.45, - "percentage_end": 0.47, - "character_start": 15432, - "character_end": 15480, - "epubcfi_start": "epubcfi(/6/4/2:15)", - "epubcfi_end": "epubcfi(/6/4/2:20)", - "created_at": "2026-01-31T10:00:00Z" - } - ] -} -``` - -### Create Highlight - -```http -POST /api/media-items/{media_id}/highlights -Authorization: Bearer -Content-Type: application/json - -{ - "selection_text": "Highlighted text...", - "start_position": "epubcfi(/6/4/2:15)", - "end_position": "epubcfi(/6/4/2:20)", - "color": "#ffff00", - "percentage_start": 0.45, - "percentage_end": 0.47 -} -``` - -### Update Highlight - -```http -PUT /api/media-items/highlights/{highlight_id} -Authorization: Bearer -Content-Type: application/json - -{ - "selection_text": "Updated text", - "color": "#00ff00" -} -``` - -### Delete Highlight - -```http -DELETE /api/media-items/highlights/{highlight_id} -Authorization: Bearer -``` - -## Ratings - -### Get Rating - -```http -GET /api/media-items/{media_id}/rating -Authorization: Bearer -``` - -**Response** (200): - -```json -{ - "rating": 8, - "user_id": "uuid", - "media_item_id": "uuid" -} -``` - -### Set Rating - -```http -POST /api/media-items/{media_id}/rating -Authorization: Bearer -Content-Type: application/json - -{ - "rating": 8 -} -``` - -**Rating Scale**: 1-10 (odd numbers = half-stars: 1=0.5★, 2=1★, 3=1.5★, ..., 9=4.5★, 10=5★) - -### Update Rating - -```http -PUT /api/media-items/{media_id}/rating -Authorization: Bearer -Content-Type: application/json - -{ - "rating": 9 -} -``` - -### Delete Rating - -```http -DELETE /api/media-items/{media_id}/rating -Authorization: Bearer -``` - -## Device Management - -### Register Device - -```http -POST /api/devices/register -Content-Type: application/json - -{ - "device_name": "My Kobo Clara", - "device_type": "kobo|koreader|web|mobile", - "device_identifier": "hardware-specific-id" -} -``` - -**Response** (201): - -```json -{ - "registration_id": "registration-uuid", - "auth_url": "https://bookhoard.com/devices/approve/abc123", - "qr_code": "data:image/png;base64,iVBORw0KG...", - "expires_in": 300, - "poll_interval": 3, - "setup_instructions": { - "koreader": "Calibre URL: https://bookhoard.com/api/sync/koreader" - } -} -``` - -Open `auth_url` (or scan the QR code) while logged in to approve; the registration expires after 5 minutes. - -### Check Registration Status - -```http -POST /api/devices/register/status -Content-Type: application/json - -{ - "registration_id": "registration-uuid" -} -``` - -**Response** (200): - -```json -{ - "status": "pending|approved", - "auth_token": "device-bearer-token...", - "device_id": "uuid", - "sync_endpoints": { - "progress": "https://bookhoard.com/api/sync/koreader/progress", - "metadata": "https://bookhoard.com/api/sync/koreader/metadata", - "bookmarks": "https://bookhoard.com/api/sync/koreader/bookmarks" - } -} -``` - -### List User Devices - -```http -GET /api/devices -Authorization: Bearer -``` - -**Response** (200): - -```json -{ - "devices": [ - { - "id": "uuid", - "device_name": "My Kobo Clara", - "device_type": "kobo", - "last_sync": "2026-01-31T10:00:00Z", - "last_seen": "2026-01-31T10:05:00Z", - "sync_enabled": true, - "auto_sync": true, - "sync_frequency_minutes": 5 - } - ] -} -``` - -### Update Device Settings - -```http -PUT /api/devices/{device_id} -Authorization: Bearer -Content-Type: application/json - -{ - "device_name": "Updated Name", - "sync_enabled": true, - "auto_sync": true, - "sync_frequency_minutes": 5 -} -``` - -### Revoke Device - -```http -DELETE /api/devices/{device_id} -Authorization: Bearer -``` - -### Get Device Sidecar Config - -Returns the `.bookhoard.json` sidecar config for a device (server endpoints, books keyed by per-format SHA-256, collections) used by the KOReader plugin to self-configure. - -```http -GET /api/devices/{device_id}/sidecar -Authorization: Bearer -``` - -Also available as a file download: - -```http -GET /api/devices/{device_id}/sidecar/download -Authorization: Bearer -``` - -## Analytics - -### Get Reading Statistics - -```http -GET /api/analytics/reading-stats?start_date={date}&end_date={date} -Authorization: Bearer -``` - -**Query Parameters**: - -- `start_date` (optional): Start date (ISO 8601 format) -- `end_date` (optional): End date (ISO 8601 format) - -**Response** (200): - -```json -{ - "pages_read": 1250, - "books_completed": 5, - "reading_time_hours": 42.5, - "sessions_count": 28, - "average_session_minutes": 91 -} -``` - -### Get Device Usage Statistics - -```http -GET /api/analytics/device-usage -Authorization: Bearer -``` - -**Response** (200): - -```json -{ - "devices": [ - { - "device_id": "uuid", - "device_name": "My Kobo Clara", - "device_type": "kobo", - "sync_count": 145, - "last_sync": "2026-02-01T10:00:00Z", - "pages_synced": 890 - } - ] -} -``` - -### Get Popular Books - -```http -GET /api/analytics/popular-books?limit={limit} -Authorization: Bearer -``` - -**Query Parameters**: - -- `limit` (optional): Number of results (default: 10) - -**Response** (200): - -```json -{ - "books": [ - { - "media_item_id": "uuid", - "title": "Book Title", - "author": "Author Name", - "cover_image_path": "/covers/book.jpg", - "read_count": 15, - "average_rating": 8.5 - } - ] -} -``` - -## Book Matching & Linking - -### Query Books for Matching - -```http -POST /api/sync/books/query -Authorization: Bearer -Content-Type: application/json - -{ - "identifiers": ["isbn:978-0345391802", "uuid:abc-123"], - "sha256": "a1b2c3d4e5f6abc123...", - "title": "The Hobbit", - "author": "J.R.R. Tolkien", - "file_size": 2456789 -} -``` - -**Response** (200): - -```json -{ - "matches": [ - { - "media_item_id": "uuid-123", - "bookhoard_uuid": "uuid-123", - "confidence": 1.0, - "match_method": "uuid_match" - } - ], - "action": "auto_link" -} -``` - -### Bulk Link Books - -```http -POST /api/sync/bulk-link-books -Authorization: Bearer -Content-Type: application/json - -{ - "links": [ - { - "unlinked_book_id": "uuid-1", - "media_item_id": "uuid-2", - "confidence_score": 1.0 - } - ] -} -``` - -**Response** (200): - -```json -{ - "results": [ - { - "unlinked_book_id": "uuid-1", - "status": "success", - "media_item_id": "uuid-2" - } - ], - "total": 1, - "successful": 1, - "failed": 0 -} -``` - -### Auto-Link Books - -```http -POST /api/sync/auto-link-books -Authorization: Bearer -Content-Type: application/json - -{ - "confidence_threshold": 0.8, - "limit": 50 -} -``` - -**Response** (200): - -```json -{ - "auto_linked": 15, - "results": [ - { - "unlinked_book_id": "uuid-1", - "title": "The Hobbit", - "matched_media_item_id": "uuid-2", - "confidence": 0.95, - "match_method": "sha256_match" - } - ] -} -``` - -### Get Unlinked Book Suggestions - -```http -GET /api/sync/unlinked-books/{id}/suggestions -Authorization: Bearer -``` - -**Response** (200): - -```json -{ - "unlinked_book_id": "uuid-1", - "title_from_device": "The Hobbit", - "sha256": "", - "suggestions": [ - { - "media_item_id": "uuid-2", - "bookhoard_uuid": "uuid-2", - "confidence": 0.95, - "match_method": "sha256_match" - } - ], - "total_suggestions": 1, - "action": "auto_link" -} -``` - -## Collections - -For complete collection management documentation, see **[Collections API](collections-api.md)**. - -**Quick Reference**: - -- `GET /api/collections` - List all collections -- `POST /api/collections` - Create new collection -- `GET /api/collections/{id}` - Get collection details -- `PUT /api/collections/{id}` - Update collection -- `DELETE /api/collections/{id}` - Delete collection -- `POST /api/collections/test-rules` - Test auto-assign rules -- `POST /api/collections/bulk-add-books` - Bulk add books to collections -- `GET /api/collections/{id}/books` - Get books in collection - -**Features**: - -- Auto-assign rules based on genre, author, series, tags, language, publisher, year -- Device shelf mappings (Kobo shelves, KOReader categories) -- Test rules before applying -- Bulk book operations - -## OPDS (Open Publication Distribution System) - -### Get Device Catalog - -```http -GET /opds/devices/{deviceId}/catalog?page={page}&per_page={per_page} -``` - -**Query Parameters**: - -- `page` (optional): Page number (default: 1) -- `per_page` (optional): Items per page (default: 50, max: 200) - -The feed is paginated via standard OPDS link relations. Clients (e.g. KOReader) -walk pages by following the `rel="next"` link until it is absent. OpenSearch -paging metadata (`totalResults`, `itemsPerPage`, `startIndex`) is also included. - -**Response** (200 - OPDS 1.2 XML): - -```xml - - - urn:uuid:device-id - Bookhoard Library - 2026-02-01T12:00:00Z - - - - - - - - - - 1814 - 50 - 51 - - - urn:uuid:bookhoard-uuid-123 - The Hobbit - J.R.R. Tolkien - 2026-02-01T10:00:00Z - - - - - - uuid-123 - abc123... - - -``` - -### Download Book with Format Conversion - -```http -GET /opds/devices/{deviceId}/download/{bookId}?format={format} -``` - -**Query Parameters**: - -- `format` (optional): Book format - `epub` (default), `kepub` - -**Response** (200): - -- Headers: - - `Content-Type`: `application/epub+zip` or `application/vnd.kobo+xml+zip` - - `Content-Disposition`: attachment; filename="The Hobbit.epub" - - `X-Bookhoard-UUID`: uuid-123 - - `X-Bookhoard-SHA256`: abc123... (original hash) - - `X-Bookhoard-KEPUB-SHA256`: xyz789... (KEPUB hash if format=kepub) - -### Search OPDS Catalog - -```http -GET /opds/devices/{deviceId}/search # OpenSearch description -GET /opds/devices/{deviceId}/search?q={query} # search results feed -``` - -When called **without** a `q` parameter, returns an OpenSearch description -document (`application/opensearchdescription+xml`). OPDS clients fetch this to -learn the search URL template, then substitute `{searchTerms}`: - -```xml - - - Bookhoard - Search the Bookhoard library - UTF-8 - UTF-8 - - -``` - -When called **with** a `q` parameter, **Response** (200 - OPDS 1.2 XML with -search results, including `opensearch:totalResults`). - -### List Available Formats - -```http -GET /opds/devices/{deviceId}/formats/{bookId} -``` - -**Response** (200): - -```json -{ - "media_item_id": "uuid-123", - "formats": [ - { - "format_type": "epub", - "file_path": "/path/to/book.epub", - "file_sha256": "abc123...", - "file_size_bytes": 2456789, - "mime_type": "application/epub+zip", - "available": true - }, - { - "format_type": "kepub", - "file_path": "/cache/book.kepub.epub", - "file_sha256": "xyz789...", - "file_size_bytes": 2478932, - "mime_type": "application/vnd.kobo+xml+zip", - "available": true - } - ] -} -``` - -## Sync Protocol - KOReader - -### KOReader Progress Sync - -```http -POST /api/sync/koreader/progress -Authorization: Bearer -Content-Type: application/json - -{ - "library_id": "optional-uuid", - "books": [ - { - "uuid": "book-uuid", - "title": "Book Title", - "authors": ["Author Name"], - "progress": 0.45, - "percentage": 0.45, - "last_read": "2026-01-30T20:00:00Z", - "chapter": 3, - "epubcfi": "epubcfi(/6/4/2:15)", - "character": 15432, - "bookmarks": [ - { - "chapter": 3, - "datetime": "2026-01-30T19:55:00Z", - "notes": "highlighted text", - "pos0": "epubcfi(/6/4/2:15)", - "pos1": "epubcfi(/6/4/2:20)", - "page": 45, - "text": "highlighted text excerpt", - "type": "highlight" - } - ], - "highlights": [], - "notes": [] - } - ] -} -``` - -**Response** (202): - -```json -{ - "sync_status": "accepted", - "books_synced": 1, - "conflicts": [ - { - "book_uuid": "book-uuid", - "conflict_type": "progress_mismatch", - "device_progress": 0.45, - "server_progress": 0.42, - "resolution": "device_wins" - } - ] -} -``` - -### KOReader Metadata Fetch - -```http -GET /api/sync/koreader/metadata/{book_uuid} -Authorization: Bearer -``` - -**Response** (200): - -```json -{ - "uuid": "book-uuid", - "title": "Book Title", - "authors": ["Author Name"], - "progress": { - "percentage": 0.42, - "character": 15432, - "epubcfi": "epubcfi(/6/4/2:15)", - "chapter": 3, - "chapter_progress": 0.234 - }, - "annotations": { - "highlights": [...], - "notes": [...], - "bookmarks": [...] - }, - "last_sync": "2026-01-30T20:00:00Z" -} -``` - -## Sync Protocol - Kobo - -> **Status: Coming Soon** — Native Kobo sync is implemented server-side but not yet supported on real devices. These endpoints are under active development and may change. - -### Kobo Markup Sync - -```http -POST /api/sync/kobo/markup -Authorization: Bearer -x-kobo-device: {"DeviceId":"device-id","Model":"Kobo Clara"} -Content-Type: application/json - -{ - "ReadingSync": [ - { - "ContentId": "book-uuid", - "PercentRead": 45.6, - "EntitlementId": "entitlement-id", - "RemainingTimeMinutes": 120, - "LastModified": "2026-01-30T20:00:00Z" - } - ], - "BookmarkSync": [ - { - "ContentId": "book-uuid", - "BookmarkText": "highlighted text", - "BookmarkType": "annotation", - "BookmarkTitle": "Chapter 3" - } - ] -} -``` - -**Response** (200): - -```json -{ - "Status": "Success", - "MarkupsSynced": 5, - "BookmarksSynced": 3 -} -``` - -### Kobo Library Fetch - -```http -GET /api/sync/kobo/library -Authorization: Bearer -``` - -**Response** (200): - -```json -{ - "library_sync": [ - { - "ContentId": "book-uuid", - "ContentType": "6", - "Title": "Book Title", - "Author": "Author Name", - "PercentRead": 42.3, - "PagesRemaining": 115, - "BookmarkCount": 3, - "LastModified": "2026-01-30T20:00:00Z" - } - ] -} -``` - -## Universal Progress - -Universal progress is the media-item progress — one row per (user, -media item) shared by every device. See [Reading Progress](#reading-progress) -and the [Position Contract](api/progress/position-contract.md). There -are no separate `/api/progress/:id` GET/POST endpoints. - -## Conflicts - -### List Conflicts - -```http -GET /api/conflicts?status=unresolved&type=progress -Authorization: Bearer -``` - -**Query Parameters**: - -- `status`: "unresolved|all" (default: "unresolved") -- `type`: "progress|note|highlight|all" (default: "all") - -**Response** (200): - -```json -{ - "conflicts": [ - { - "id": "conflict-uuid", - "media_item_id": "book-uuid", - "media_item_title": "Book Title", - "conflict_type": "progress", - "conflict_data": { - "koreader": { - "source": "koreader", - "timestamp": "2026-01-30T20:10:00Z", - "data": { - "percentage": 0.45, - "epubcfi": "epubcfi(/6/4/2:15)", - "character": 15432 - } - }, - "kobo": { - "source": "kobo", - "timestamp": "2026-01-30T20:05:00Z", - "data": { - "percentage": 0.42 - } - } - }, - "resolution_status": "unresolved", - "created_at": "2026-01-30T20:10:05Z" - } - ], - "total": 1, - "unresolved": 1 -} -``` - -### Get Conflict Details - -```http -GET /api/conflicts/{conflict_id} -Authorization: Bearer -``` - -### Resolve Conflict - -```http -POST /api/conflicts/{conflict_id}/resolve -Authorization: Bearer -Content-Type: application/json - -{ - "winner": "koreader|kobo|web|manual", - "manual_data": { - "percentage": 0.43, - "epubcfi": "epubcfi(/6/4/2:20)", - "character": 15500 - }, - "apply_to_all_future_conflicts": false, - "reason": "user chose more recent progress" -} -``` - -**Response** (200): - -```json -{ - "conflict_resolved": true, - "applied_to": { - "progress": true, - "annotations": false - }, - "devices_synced": ["device-1", "device-2"] -} -``` - -### Delete Conflict - -```http -DELETE /api/conflicts/{conflict_id} -Authorization: Bearer -``` - -### Dismiss All Resolved - -```http -DELETE /api/conflicts/dismiss-resolved -Authorization: Bearer -``` - -## Sync Queue - -### List Queue Items - -```http -GET /api/queue/items?limit=50&offset=0 -Authorization: Bearer -``` - -**Response** (200): - -```json -{ - "items": [ - { - "id": "uuid", - "device_id": "device-uuid", - "device_name": "My Kobo", - "media_item_id": "book-uuid", - "sync_type": "progress", - "sync_data": {}, - "priority": 5, - "attempts": 0, - "max_attempts": 3, - "status": "pending", - "error_message": null, - "created_at": "2026-01-31T10:00:00Z" - } - ], - "total": 100 -} -``` - -### Process Queue Item - -```http -POST /api/queue/items/{queue_item_id}/process -Authorization: Bearer -``` - -### Retry Queue Item - -```http -POST /api/queue/items/{queue_item_id}/retry -Authorization: Bearer -``` - -### Delete Queue Item - -```http -DELETE /api/queue/items/{queue_item_id} -Authorization: Bearer -``` - -### Clear Queue - -```http -DELETE /api/queue/clear -Authorization: Bearer -``` - -### Clear Failed Items - -```http -DELETE /api/queue/clear-failed -Authorization: Bearer -``` - -### Get Queue Stats - -```http -GET /api/queue/stats -Authorization: Bearer -``` - -**Response** (200): - -```json -{ - "pending": 15, - "processing": 2, - "failed": 3, - "completed": 100, - "total": 120 -} -``` - -## System Settings & Configuration - -### List All Settings - -Returns every tunable setting with current value and metadata (type, range, category, group, description, `requires_restart`, `is_default`). - -```http -GET /api/system/settings -Authorization: Bearer -``` - -**Response** (200): - -```json -[ - { - "key": "scan_poll_interval_seconds", - "value": "60", - "type": "int", - "min": "1", - "max": "3600", - "requires_restart": false, - "category": "scanner", - "group": "Scanning", - "description": "How often to scan all libraries (seconds)", - "is_default": true - } -] -``` - -### Update a Setting - -Type-aware validation (int range, bool parse, IANA timezone for `default_timezone`), persists the value, reloads the registry, and reports whether a restart is needed. - -```http -PUT /api/system/settings -Authorization: Bearer -Content-Type: application/json - -{ - "key": "scan_poll_interval_seconds", - "value": "30" -} -``` - -**Response** (200): the updated entry plus `reload_required`. - -Setting categories: scanner (`scan_poll_interval_seconds`, `auto_scan_enabled`), general (`default_timezone`), security (session duration, password rules, auth rate limit, login lockout), api (OPDS page sizes, device rate limits), sync (annotation tombstone TTL, sync queue interval/batch), performance (conversion cache TTL, worker pool size/capacity). See [System Settings API](api/system/settings.md) for the full catalog. - -### Get / Update Raw System Config - -Flat key/value configuration (e.g. `base_url`), including keys without registry metadata. - -```http -GET /api/system/config -PUT /api/system/config -Authorization: Bearer -``` - -## Hash Conflicts - -Duplicate content discovered during hashing (import, rescan, or the startup backfill) is grouped into hash conflicts for an explicit keep/merge decision. Files on disk are never deleted. - -### List Hash Conflicts - -```http -GET /api/admin/hash-conflicts -Authorization: Bearer -``` - -**Response** (200): `{ "conflicts": [ { id, library_id, library_name, sha256, created_at, items: [ { id, title, author, file_path, file_size, created_at, progress_count, highlight_count, bookmark_count, note_count, collection_count } ] } ], "total": n }` - -### Resolve Hash Conflict - -```http -POST /api/admin/hash-conflicts/:id/resolve -Authorization: Bearer -Content-Type: application/json - -{ - "action": "keep", - "keep_uuid": "media-item-uuid-to-keep" -} -``` - -- `action=keep` — merge every other copy's child rows (progress, highlights, bookmarks, notes, collections) into the kept item, then delete the losers -- `action=keep_all` — copies are intentional; dismiss the conflict - -**Errors**: `400` (bad ID / missing `keep_uuid`), `404` (not found), `409` (already resolved). - -## WebSocket - -### Connect to WebSocket - -``` -WS /ws/sync?token= -``` - -### Message Format - -**Client → Server (Heartbeat)**: - -```json -{ - "type": "ping" -} -``` - -**Server → Client (Progress Update)**: - -```json -{ - "type": "progress_update", - "timestamp": "2026-01-31T10:00:00Z", - "data": { - "book_id": "uuid", - "progress": { - "percentage": 0.45678, - "epubcfi": "epubcfi(/6/4/2:15)", - "chapter": 3 - }, - "annotations": {} - }, - "source_device": { - "id": "device-uuid", - "name": "My Kobo", - "type": "kobo" - } -} -``` - -**Server → Client (Conflict Detected)**: - -```json -{ - "type": "conflict", - "timestamp": "2026-01-31T10:00:00Z", - "data": { - "book_id": "uuid", - "conflict_id": "uuid", - "conflict_type": "progress" - } -} -``` - -**Server → Client (Pong)**: - -```json -{ - "type": "pong" -} -``` - -## Error Responses - -All endpoints return standardized error responses: - -```json -{ - "error": "Error message", - "message": "Detailed error information (if available)", - "code": "ERROR_CODE" -} -``` - -### HTTP Status Codes - -- **200**: OK - Request successful -- **201**: Created - Resource created successfully -- **204**: No Content - Successful deletion or update with no content -- **400**: Bad Request - Invalid request parameters -- **401**: Unauthorized - Missing or invalid authentication -- **403**: Forbidden - Insufficient permissions -- **404**: Not Found - Resource does not exist -- **409**: Conflict - Resource conflict (e.g., duplicate) -- **422**: Unprocessable Entity - Validation error -- **429**: Too Many Requests - Rate limit exceeded -- **500**: Internal Server Error - Server error - -### Rate Limiting - -**Per-Device Limits**: - -- Sync requests: 60/minute -- Progress updates: 120/minute -- Metadata requests: 30/minute - -**Per-User Limits**: - -- All requests: 300/minute -- Conflict resolutions: 10/minute -- Device registrations: 5/hour - -**Rate Limit Headers**: - -``` -X-RateLimit-Limit: 60 -X-RateLimit-Remaining: 45 -X-RateLimit-Reset: 60 -``` - -## Bruno v3.0 Collections - -Complete API test collections are available in the `bruno/` directory: - -``` -bruno/ -├── user/ # Authentication & profiles -├── admin/ # Admin operations -├── library/ # Library management -├── media-items/ # Media content -├── progress/ # Reading progress -├── notes/ # Notes API -├── highlights/ # Highlights API -├── ratings/ # Ratings API -├── devices/ # Device management -├── sync-koreader/ # KOReader sync protocol -├── sync-kobo/ # Kobo sync protocol -├── conflicts/ # Conflict resolution -├── queue/ # Sync queue management -└── collection.yml # Main collection file -``` - -## Testing with Bruno OpenCollection YAML - -Install Bruno CLI: - -```bash -npm install -g @usebruno/cli -``` - -Run all tests: - -```bash -bruno run -``` - -Run specific collection: - -```bash -bruno run bruno/devices/ -``` - -## Additional Resources - -- [README.md](../../README.md) - Getting started guide -- [Sync Guide](../user/sync-guide.md) - Sync concepts and conflict resolution -- [KOReader Setup](../user/devices/koreader-setup.md) - KOReader device setup -- [Kobo Setup](../user/devices/kobo-setup.md) - Kobo device setup (native sync coming soon) - ---- - -**Document Version**: 1.0 -**Last Updated**: 2026-01-31 -**API Version**: v1.0 diff --git a/docs/developer/api/api-reference.md b/docs/developer/api/api-reference.md index 335733f3..8a1c66c7 100644 --- a/docs/developer/api/api-reference.md +++ b/docs/developer/api/api-reference.md @@ -256,11 +256,11 @@ See [KOReader Sync](koreader/) and [Sync Protocol](sync/koreader-protocol.md) See [Kobo Sync](kobo/) and [Sync Protocol](sync/kobo-protocol.md) -- POST /api/sync/kobo/markup - Sync markup highlights -- POST /api/sync/kobo/bookmark - Sync bookmarks -- POST /api/sync/kobo/v1/analytics/gettests - Analytics endpoint -- GET /api/sync/kobo/v1/initialization - Initialize Kobo sync -- POST /api/sync/kobo/sync-from-server - Push content to device +- POST /api/sync/kobo/:token/markup - Sync markup highlights +- POST /api/sync/kobo/:token/bookmark - Sync bookmarks +- POST /api/sync/kobo/:token/v1/analytics/gettests - Analytics endpoint +- GET /api/sync/kobo/:token/v1/initialization - Initialize Kobo sync +- POST /api/sync/kobo/:token/sync-from-server - Push content to device ## WebSocket diff --git a/docs/developer/api/highlights/delete_highlight.md b/docs/developer/api/highlights/delete_highlight.md index f844d0c1..2046d4b1 100644 --- a/docs/developer/api/highlights/delete_highlight.md +++ b/docs/developer/api/highlights/delete_highlight.md @@ -2,7 +2,7 @@ Delete a highlight. -**Endpoint**: `DELETE /api/media-items/highlights/{highlight_id}` +**Endpoint**: `DELETE /api/media-items/:id/highlights/:highlightId` **Auth**: Required ## Path Parameters diff --git a/docs/developer/api/highlights/update_highlight.md b/docs/developer/api/highlights/update_highlight.md index bb12fdc8..a7587871 100644 --- a/docs/developer/api/highlights/update_highlight.md +++ b/docs/developer/api/highlights/update_highlight.md @@ -2,7 +2,7 @@ Update an existing highlight. -**Endpoint**: `PUT /api/media-items/highlights/{highlight_id}` +**Endpoint**: `PUT /api/media-items/:id/highlights/:highlightId` **Auth**: Required **Content-Type**: `application/json` diff --git a/docs/developer/api/kobo/analytics_gettests.md b/docs/developer/api/kobo/analytics_gettests.md index 934887c4..ff76e426 100644 --- a/docs/developer/api/kobo/analytics_gettests.md +++ b/docs/developer/api/kobo/analytics_gettests.md @@ -4,7 +4,7 @@ Kobo analytics endpoint (device compatibility). -**Endpoint**: `POST /api/sync/kobo/v1/analytics/gettests` +**Endpoint**: `POST /api/sync/kobo/:token/v1/analytics/gettests` **Auth**: Required (Device authentication) ## Device Authentication diff --git a/docs/developer/api/kobo/bookmark_sync.md b/docs/developer/api/kobo/bookmark_sync.md index ac5994aa..c09ffec7 100644 --- a/docs/developer/api/kobo/bookmark_sync.md +++ b/docs/developer/api/kobo/bookmark_sync.md @@ -4,7 +4,7 @@ Sync bookmarks from Kobo device. -**Endpoint**: `POST /api/sync/kobo/bookmark` +**Endpoint**: `POST /api/sync/kobo/:token/bookmark` **Auth**: Required (Device authentication) ## Device Authentication diff --git a/docs/developer/api/kobo/initialization.md b/docs/developer/api/kobo/initialization.md index 074a5c8a..2533bdef 100644 --- a/docs/developer/api/kobo/initialization.md +++ b/docs/developer/api/kobo/initialization.md @@ -4,7 +4,7 @@ Initialize Kobo device sync. -**Endpoint**: `GET /api/sync/kobo/v1/initialization` +**Endpoint**: `GET /api/sync/kobo/:token/v1/initialization` **Auth**: Required (Device authentication) ## Device Authentication @@ -22,7 +22,7 @@ This endpoint requires device authentication (not user JWT). Kobo devices authen ### Example Request ```http -GET /api/sync/kobo/v1/initialization +GET /api/sync/kobo/:token/v1/initialization X-Device-ID: 550e8400-e29b-41d4-a716-446655440000 X-Device-Key: device-auth-key ``` diff --git a/docs/developer/api/kobo/markup_sync.md b/docs/developer/api/kobo/markup_sync.md index 6782bcde..61787dc2 100644 --- a/docs/developer/api/kobo/markup_sync.md +++ b/docs/developer/api/kobo/markup_sync.md @@ -4,7 +4,7 @@ Sync markup highlights and annotations from Kobo device. -**Endpoint**: `POST /api/sync/kobo/markup` +**Endpoint**: `POST /api/sync/kobo/:token/markup` **Auth**: Required (Device authentication) ## Device Authentication diff --git a/docs/developer/api/kobo/sync_from_server.md b/docs/developer/api/kobo/sync_from_server.md index e915b80a..ac21ba81 100644 --- a/docs/developer/api/kobo/sync_from_server.md +++ b/docs/developer/api/kobo/sync_from_server.md @@ -4,7 +4,7 @@ Push content and metadata to Kobo device. -**Endpoint**: `POST /api/sync/kobo/sync-from-server` +**Endpoint**: `POST /api/sync/kobo/:token/sync-from-server` **Auth**: Required (Device authentication) ## Device Authentication diff --git a/docs/developer/api/media-items/download_media_item.md b/docs/developer/api/media-items/download_media_item.md deleted file mode 100644 index 1f1e8d74..00000000 --- a/docs/developer/api/media-items/download_media_item.md +++ /dev/null @@ -1,48 +0,0 @@ -# Download Media Item - -Download a media item file (EPUB, PDF, etc.) from the Bookhoard server. - -Book files are served by the authenticated file route, the same one the web reader uses. Build the URL from the media item's `library_id` and relative `file_path` (both returned by the media item list/get endpoints): - -**Endpoint**: `GET /uploads/library-{library_id}/{file_path}` -**Auth**: Required (JWT - Bearer header or session cookie) - -The `file_path` segments are URL-escaped individually; slashes are preserved. `cover_image_path` uses the same route. - -## Path Parameters - -| Parameter | Type | Required | Description | -| ----------- | ------ | -------- | ------------------------------------ | -| library_id | string | Yes | Library UUID (the item's library) | -| file_path | string | Yes | The item's relative `file_path` | - -## Response - -**Success (200 OK)**: Binary file data - -**Response Headers**: - -- `Content-Type`: MIME type by file extension (`application/epub+zip`, `application/pdf`, …; `application/octet-stream` fallback) -- `Cache-Control`: `public, max-age=86400` - -## Error Responses - -| Code | Description | -| ---- | --------------------------- | -| 400 | Invalid library ID or path | -| 401 | Missing/invalid token | -| 404 | File not found on disk | - -## Example - -```bash -curl -O -H "Authorization: Bearer $TOKEN" \ - "http://localhost:8765/uploads/library/550e8400-.../books/1984.epub" -``` - -(URL shape: `/uploads/library-{uuid}/{escaped-relative-path}`.) - -## Notes - -- **Do not rely on `GET /api/media-items/:id/download`** — it appears in older docs but is **not registered**; `MediaHandler.DownloadBook` exists as dead code. Use the file route above. -- OPDS-capable devices may alternatively use the device-authenticated `GET /opds/devices/{deviceId}/download/{bookId}`, which supports on-the-fly format conversion (epub, kepub, pdf, cbz). diff --git a/docs/developer/api/media-items/get_audio_chapters.md b/docs/developer/api/media-items/get_audio_chapters.md index a63e2d05..1e62cddb 100644 --- a/docs/developer/api/media-items/get_audio_chapters.md +++ b/docs/developer/api/media-items/get_audio_chapters.md @@ -23,7 +23,7 @@ which file model produced them. "tracks": [ { "index": 0, - "url": "/api/media-items/{id}/audio/0", + "url": "/api/media-items/{id}/audio/{trackIndex}", "title": "01 First", "path": "Author/Title/01 First.mp3", "start": 0.0, diff --git a/docs/developer/api/media-items/get_audio_track.md b/docs/developer/api/media-items/get_audio_track.md index 11d72e7c..8a18b7f5 100644 --- a/docs/developer/api/media-items/get_audio_track.md +++ b/docs/developer/api/media-items/get_audio_track.md @@ -40,5 +40,5 @@ answers **403** on every track (same gate as the download endpoint). ```bash curl -H "Authorization: Bearer $TOKEN" \ -H "Range: bytes=0-1023" \ - "http://localhost:8765/api/media-items/550e8400-.../audio/0" + "http://localhost:8765/api/media-items/550e8400-.../audio/{track_index}" ``` diff --git a/docs/developer/api/notes/delete_note.md b/docs/developer/api/notes/delete_note.md index d4f15cc8..279558e1 100644 --- a/docs/developer/api/notes/delete_note.md +++ b/docs/developer/api/notes/delete_note.md @@ -2,7 +2,7 @@ Delete a note. -**Endpoint**: `DELETE /api/media-items/notes/{note_id}` +**Endpoint**: `DELETE /api/media-items/:id/notes/:noteId` **Auth**: Required ## Path Parameters diff --git a/docs/developer/api/notes/update_note.md b/docs/developer/api/notes/update_note.md index a4a49259..c0aa370d 100644 --- a/docs/developer/api/notes/update_note.md +++ b/docs/developer/api/notes/update_note.md @@ -2,7 +2,7 @@ Update an existing note. -**Endpoint**: `PUT /api/media-items/notes/{note_id}` +**Endpoint**: `PUT /api/media-items/:id/notes/:noteId` **Auth**: Required **Content-Type**: `application/json` diff --git a/docs/developer/api/sync/kobo-protocol.md b/docs/developer/api/sync/kobo-protocol.md index eea87469..089d29eb 100644 --- a/docs/developer/api/sync/kobo-protocol.md +++ b/docs/developer/api/sync/kobo-protocol.md @@ -6,7 +6,7 @@ Kobo uses a proprietary sync protocol with JSON payloads. ## Kobo Markup Sync -**Endpoint**: `POST /api/sync/kobo/markup` +**Endpoint**: `POST /api/sync/kobo/:token/markup` **Auth**: Device token required **Content-Type**: `application/json` @@ -33,7 +33,7 @@ Kobo uses a proprietary sync protocol with JSON payloads. ### Example Request ```http -POST /api/sync/kobo/markup +POST /api/sync/kobo/:token/markup Authorization: Bearer device-token x-kobo-device: {"DeviceId":"device-id","Model":"Kobo Clara"} Content-Type: application/json diff --git a/docs/operations/troubleshooting.md b/docs/operations/troubleshooting.md index d77c677c..8244ad88 100644 --- a/docs/operations/troubleshooting.md +++ b/docs/operations/troubleshooting.md @@ -168,10 +168,10 @@ podman-compose ps # or: docker compose ps podman-compose exec db psql -U postgres -d bookhoard -c "SELECT 1;" # or: docker compose exec db ... # Test API endpoint: -curl -s http://localhost:8765/api/libraries/visible +curl -s http://localhost:8765/api/libraries/visibility # For remote servers, use actual IP: -curl -s http://SERVER_IP:8765/api/libraries/visible +curl -s http://SERVER_IP:8765/api/libraries/visibility ``` ## 🚀 Quick Verification @@ -186,7 +186,7 @@ podman-compose ps # or: docker compose ps podman-compose exec db psql -U postgres -d bookhoard -c "SELECT 1;" # or: docker compose exec db ... # Test API endpoint -curl -s http://localhost:8765/api/libraries/visible +curl -s http://localhost:8765/api/libraries/visibility # Check application logs docker compose logs app @@ -209,7 +209,7 @@ docker compose up -d # 4. Verify docker compose ps -curl -s http://localhost:8765/api/libraries/visible +curl -s http://localhost:8765/api/libraries/visibility ``` ## 🌐 Production Deployment @@ -319,7 +319,7 @@ The current setup follows best practices: When properly set up, you should see: 1. **Healthy containers**: `docker compose ps` shows "Up" status -2. **Working API**: `curl http://localhost:8765/api/libraries/visible` returns JSON +2. **Working API**: `curl http://localhost:8765/api/libraries/visibility` returns JSON 3. **New tables**: `media_highlights` and `media_notes` exist with proper schema 4. **No port conflicts**: Application binds to port 8765 without errors 5. **Database ready**: PostgreSQL accepts connections and serves requests