docs(api): real progress endpoint contract; remove nonexistent-endpoint docs
The progress documentation described GET/POST /api/progress/:id — endpoints that do not exist in the router — while the endpoint every client actually uses (GET/PUT/DELETE /api/media-items/:id/progress) had no field-level docs at all. New: - progress/position-contract.md: the canonical position model — server as position authority, the three-tier submission (percentage / context_text / epubcfi), ingest verification and healing, the restore handles, the OPF spine numbering hazard (canonical CFI spine steps include linear="no" items; clients resolve documents by anchor_href and land by css_selector + char_offset, never by spine step), the offset currencies (UTF-16 at the wire, runes internal), and context_text rules. - progress/get_media_progress.md and update_media_progress.md: the real endpoints with full field tables, conditionality of the restore handles, the first-page anti-clobber guard, and healed-response semantics. - progress/delete_media_progress.md: the real DELETE route. - koreader/sync_progress.md and koreader/get_metadata.md rewritten to the actual payloads: the plugin sends a single-book array whose "epubcfi" field is a CRE xpointer; the metadata response navigates via koreader_xpointer (canonical CFI converted back to CRE), with page as the canonical locator for fixed-layout books. Removed: the five files documenting the nonexistent /api/progress/:id GET/POST/DELETE endpoints. Kept get_progress_history.md (that route exists). The legacy developer/api-reference.md and the indexed api/api-reference.md progress sections now match the wire and link the new docs; the duplicate "Universal Progress" section points at Reading Progress.
This commit is contained in:
+40
-108
@@ -394,6 +394,11 @@ Authorization: Bearer <token>
|
|||||||
|
|
||||||
## Reading Progress
|
## 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
|
### Get Reading Progress
|
||||||
|
|
||||||
```http
|
```http
|
||||||
@@ -405,57 +410,56 @@ Authorization: Bearer <token>
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"id": "uuid",
|
||||||
"media_item_id": "uuid",
|
"media_item_id": "uuid",
|
||||||
"user_id": "uuid",
|
"user_id": "uuid",
|
||||||
"current_page": 45,
|
"percentage": 0.045,
|
||||||
"total_pages": 200,
|
"epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/58/1:598)",
|
||||||
"percentage": 0.225,
|
"context_text": "from day to day, but there was none in which ...",
|
||||||
"character_offset": 15432,
|
"character_offset": 16375,
|
||||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
"current_page": null,
|
||||||
"chapter": 3,
|
"total_pages": null,
|
||||||
"chapter_progress": 0.5,
|
"chapter": null,
|
||||||
"last_read_at": "2026-01-31T10:00:00Z",
|
"chapter_progress": null,
|
||||||
"format_group": "reflowable",
|
"format_group": "reflowable",
|
||||||
"viewport_y": 0.12,
|
"total_characters": 592216,
|
||||||
"zoom_level": 1.0
|
"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
|
### Update Reading Progress
|
||||||
|
|
||||||
```http
|
```http
|
||||||
PUT /api/media-items/{media_id}/progress
|
PUT /api/media-items/{media_id}/progress
|
||||||
Authorization: Bearer <token>
|
Authorization: Bearer <token>
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
|
||||||
"source": "web",
|
|
||||||
"location": {
|
|
||||||
"percentage": 0.45678,
|
|
||||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
|
||||||
"character": 15432,
|
|
||||||
"chapter": 3,
|
|
||||||
"page": 89,
|
|
||||||
"total_pages": 200
|
|
||||||
},
|
|
||||||
"device_metadata": {
|
|
||||||
"device_type": "web",
|
|
||||||
"user_agent": "Mozilla/5.0..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response** (200):
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"sync_status": "success",
|
"percentage": 0.0415,
|
||||||
"progress_updated": true,
|
"context_text": "was at war with one of these Powers it was generally a",
|
||||||
"devices_notified": ["device-1", "device-2"],
|
"epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/62/1:456)"
|
||||||
"broadcast": true
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
### Delete Reading Progress
|
||||||
|
|
||||||
```http
|
```http
|
||||||
@@ -1297,82 +1301,10 @@ Authorization: Bearer <device_token>
|
|||||||
|
|
||||||
## Universal Progress
|
## Universal Progress
|
||||||
|
|
||||||
### Get Universal Progress
|
Universal progress is the media-item progress — one row per (user,
|
||||||
|
media item) shared by every device. See [Reading Progress](#reading-progress)
|
||||||
```http
|
and the [Position Contract](api/progress/position-contract.md). There
|
||||||
GET /api/progress/{book_uuid}
|
are no separate `/api/progress/:id` GET/POST endpoints.
|
||||||
Authorization: Bearer <token>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response** (200):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"book_id": "book-uuid",
|
|
||||||
"format_group": "reflowable",
|
|
||||||
"universal_progress": 0.45678,
|
|
||||||
"location_references": {
|
|
||||||
"percentage": 0.45678,
|
|
||||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
|
||||||
"character": 15432,
|
|
||||||
"chapter": 3,
|
|
||||||
"chapter_progress": 0.234,
|
|
||||||
"viewport_y": 0.12
|
|
||||||
},
|
|
||||||
"device_progress": {
|
|
||||||
"koreader": {
|
|
||||||
"percentage": 0.45678,
|
|
||||||
"last_sync": "2026-01-30T20:00:00Z"
|
|
||||||
},
|
|
||||||
"kobo": {
|
|
||||||
"percentage": 45.6,
|
|
||||||
"last_sync": "2026-01-30T19:55:00Z"
|
|
||||||
},
|
|
||||||
"web": {
|
|
||||||
"display_page": 89,
|
|
||||||
"total_pages": 200,
|
|
||||||
"last_sync": "2026-01-30T20:05:00Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"annotations": {
|
|
||||||
"highlights": [...],
|
|
||||||
"notes": [...],
|
|
||||||
"bookmarks": [...]
|
|
||||||
},
|
|
||||||
"conflicts": [
|
|
||||||
{
|
|
||||||
"id": "conflict-uuid",
|
|
||||||
"type": "progress",
|
|
||||||
"resolved": false,
|
|
||||||
"sources": ["koreader", "kobo"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Update Universal Progress
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/progress/{book_uuid}
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"source": "web|koreader|kobo|mobile",
|
|
||||||
"location": {
|
|
||||||
"percentage": 0.45678,
|
|
||||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
|
||||||
"character": 15432,
|
|
||||||
"chapter": 3,
|
|
||||||
"page": 89,
|
|
||||||
"total_pages": 200
|
|
||||||
},
|
|
||||||
"device_metadata": {
|
|
||||||
"device_type": "web",
|
|
||||||
"user_agent": "..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Conflicts
|
## Conflicts
|
||||||
|
|
||||||
|
|||||||
@@ -117,10 +117,14 @@ See [Media Item Operations](media-items/)
|
|||||||
|
|
||||||
## Reading Progress
|
## Reading Progress
|
||||||
|
|
||||||
See [Progress Tracking](progress/)
|
See [Progress Tracking](progress/) — in particular the
|
||||||
|
[Position Contract](progress/position-contract.md) (verification,
|
||||||
|
healing, the OPF spine numbering hazard, and offset currencies) before
|
||||||
|
writing a client.
|
||||||
|
|
||||||
- GET /api/progress/:id - Get universal progress
|
- GET /api/media-items/:id/progress - Get reading progress + restore handles
|
||||||
- POST /api/progress/:id - Update universal progress
|
- PUT /api/media-items/:id/progress - Submit reading progress (verified/healed server-side)
|
||||||
|
- DELETE /api/media-items/:id/progress - Delete reading progress
|
||||||
- GET /api/progress/:id/history - Get progress history
|
- GET /api/progress/:id/history - Get progress history
|
||||||
|
|
||||||
## Notes & Highlights
|
## Notes & Highlights
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
# Get Metadata
|
# Get Metadata
|
||||||
|
|
||||||
Get metadata for a book from KOReader device.
|
Get a book's stored progress and annotations for a KOReader device —
|
||||||
|
the pull half of the device sync. The reference client calls this after
|
||||||
|
linking a book via [Resolve Book](resolve_book.md) and navigates to the
|
||||||
|
returned position.
|
||||||
|
|
||||||
**Endpoint**: `GET /api/sync/koreader/metadata/:uuid`
|
**Endpoint**: `GET /api/sync/koreader/metadata/:uuid`
|
||||||
**Auth**: Required (Device authentication)
|
**Auth**: Required (Device authentication — `Authorization: Bearer {device_token}`)
|
||||||
|
|
||||||
## Path Parameters
|
## Path Parameters
|
||||||
|
|
||||||
@@ -11,41 +14,55 @@ Get metadata for a book from KOReader device.
|
|||||||
| --------- | ------------- | -------- | ----------- |
|
| --------- | ------------- | -------- | ----------- |
|
||||||
| uuid | string (UUID) | Yes | Book UUID |
|
| uuid | string (UUID) | Yes | Book UUID |
|
||||||
|
|
||||||
## Device Authentication
|
|
||||||
|
|
||||||
This endpoint requires device authentication (not user JWT). Devices authenticate using their device credentials.
|
|
||||||
|
|
||||||
## Request Headers
|
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
|
||||||
| ------------ | ------ | -------- | ------------------------- |
|
|
||||||
| X-Device-ID | string | Yes | Device UUID |
|
|
||||||
| X-Device-Key | string | Yes | Device authentication key |
|
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /api/sync/koreader/metadata/550e8400-e29b-41d4-a716-446655440000
|
GET /api/sync/koreader/metadata/774641f9-317b-4087-8e04-53bb4392ae56
|
||||||
X-Device-ID: 550e8400-e29b-41d4-a716-446655440000
|
Authorization: Bearer {device_token}
|
||||||
X-Device-Key: device-auth-key
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Response (200 OK)
|
## Response (200 OK)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "uuid",
|
"uuid": "774641f9-317b-4087-8e04-53bb4392ae56",
|
||||||
"title": "Book Title",
|
"title": "1984",
|
||||||
"authors": ["Author Name"],
|
"author": "George Orwell",
|
||||||
"path": "/path/to/book.epub",
|
"progress": {
|
||||||
"file_size": 1234567,
|
"percentage": 0.045,
|
||||||
"modified_at": "2026-02-08T10:00:00Z"
|
"koreader_xpointer": "/body/DocFragment[1]/body/p[29]/text().598",
|
||||||
|
"chapter": null,
|
||||||
|
"chapter_progress": null,
|
||||||
|
"page": null,
|
||||||
|
"total_pages": null
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"highlights": []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Progress Object
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
| ----- | ---- | ----------- |
|
||||||
|
| `percentage` | float | Stored position as a book fraction. |
|
||||||
|
| `koreader_xpointer` | string | The stored canonical position converted back to a CRE xpointer (UTF-16 `text().N` offset). **The device should navigate to this.** Reflowable books only. |
|
||||||
|
| `epubcfi` | string | The stored canonical CFI, when the conversion to a CRE xpointer is unavailable. Fallback after `koreader_xpointer`. |
|
||||||
|
| `character` | int | Book-wide rune offset (internal currency). |
|
||||||
|
| `chapter`, `chapter_progress` | int, float | Chapter position when known. |
|
||||||
|
| `page`, `total_pages` | int | Fixed-layout page position — the canonical locator for image-based books (CFI/xpointer are omitted for them). |
|
||||||
|
|
||||||
|
`progress` is `null` when the book has no stored progress.
|
||||||
|
|
||||||
|
The `annotations` object carries device-format highlights/bookmarks/notes
|
||||||
|
synced from other clients; its presence depends on annotation sync being
|
||||||
|
enabled.
|
||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
| ---- | ---------------------------- |
|
| ---- | ----------- |
|
||||||
|
| 400 | Invalid book UUID |
|
||||||
| 401 | Device authentication failed |
|
| 401 | Device authentication failed |
|
||||||
| 404 | Book or device not found |
|
| 404 | Book not found |
|
||||||
|
|||||||
@@ -1,63 +1,87 @@
|
|||||||
# Sync Progress
|
# Sync Progress
|
||||||
|
|
||||||
Sync reading progress from KOReader device.
|
Push reading progress from a KOReader device.
|
||||||
|
|
||||||
**Endpoint**: `POST /api/sync/koreader/progress`
|
**Endpoint**: `POST /api/sync/koreader/progress`
|
||||||
**Auth**: Required (Device authentication)
|
**Auth**: Required (Device authentication — `Authorization: Bearer {device_token}`)
|
||||||
|
|
||||||
## Device Authentication
|
This is the device-native tier of the [Position
|
||||||
|
Contract](../progress/position-contract.md): the KOReader payload carries
|
||||||
This endpoint requires device authentication (not user JWT). Devices authenticate using their device credentials.
|
a CRE xpointer and the server converts it to the canonical standard CFI,
|
||||||
|
verifies it against the submitted `context_text`, and heals it on
|
||||||
|
mismatch — exactly like every other client.
|
||||||
|
|
||||||
## Request Body
|
## Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
| --------- | ------------- | -------- | ------------------------- |
|
| --------- | ------ | -------- | ----------- |
|
||||||
| device_id | string (UUID) | Yes | Device UUID |
|
| `books` | array | Yes | One book object (the reference client sends a single-element array). |
|
||||||
| progress | array | Yes | Array of progress objects |
|
| `sync_mode`| string | No | `immediate` (default) or `manual`. |
|
||||||
|
|
||||||
### Progress Object
|
### Book Object
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
| ----------- | ------- | -------- | ---------------------------------- |
|
| ----- | ---- | -------- | ----------- |
|
||||||
| book | string | Yes | Book identifier (filename or UUID) |
|
| `uuid` | string (UUID) | No | Bookhoard UUID, once the device has linked the book via [Resolve Book](resolve_book.md). |
|
||||||
| percent | float | Yes | Progress percentage (0-100) |
|
| `sha256` | string | Yes | File content hash (64 hex chars) — the primary book identity. |
|
||||||
| page | integer | No | Current page number |
|
| `title` | string | No | Document title. |
|
||||||
| total_pages | integer | No | Total pages in document |
|
| `authors` | array | No | Author names. |
|
||||||
| date_read | string | No | ISO 8601 timestamp of last read |
|
| `percentage` | float | Yes | Position as a fraction of the book (0..1). |
|
||||||
| updated_at | string | Yes | ISO 8601 timestamp |
|
| `context_text` | string | No | Up to 100 whitespace-normalized chars from the current position — enables the server's verification/healing. Strongly recommended. |
|
||||||
|
| `page` | int | No | Current page (fixed-layout books). |
|
||||||
|
| `total_pages` | int | No | Page count (fixed-layout books). |
|
||||||
|
| `epubcfi` | string | Reflowable only | **A CRE xpointer** (`/body/DocFragment[N]/body/...`), not a CFI — the field name is historical. Fixed-layout books must omit it and carry their position in `page`/`total_pages`. |
|
||||||
|
| `file_path` | string | No | Device-local file path (informational). |
|
||||||
|
| `device_info` | object | No | `{ koreader_version, device_model }`. |
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
```json
|
```http
|
||||||
{
|
POST /api/sync/koreader/progress
|
||||||
"device_id": "550e8400-e29b-41d4-a716-446655440000",
|
Authorization: Bearer {device_token}
|
||||||
"progress": [
|
Content-Type: application/json
|
||||||
{
|
|
||||||
"book": "book.epub",
|
|
||||||
"percent": 75.5,
|
|
||||||
"page": 150,
|
|
||||||
"total_pages": 200,
|
|
||||||
"date_read": "2026-02-08T10:00:00Z",
|
|
||||||
"updated_at": "2026-02-08T10:00:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Response (200 OK)
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "Progress synced successfully",
|
"books": [
|
||||||
"synced_count": 1
|
{
|
||||||
|
"uuid": "774641f9-317b-4087-8e04-53bb4392ae56",
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"title": "1984",
|
||||||
|
"authors": ["George Orwell"],
|
||||||
|
"percentage": 0.045,
|
||||||
|
"context_text": "from day to day, but there was none in which Goldstein was not the principal figure. He was the prim",
|
||||||
|
"page": 14,
|
||||||
|
"total_pages": 311,
|
||||||
|
"epubcfi": "/body/DocFragment[1]/body/p[29]/text().598",
|
||||||
|
"device_info": {
|
||||||
|
"koreader_version": "v2026.07.1",
|
||||||
|
"device_model": "emulator"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sync_mode": "immediate"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Response (202 Accepted)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sync_status": "ok",
|
||||||
|
"books_synced": 1,
|
||||||
|
"timestamp": "2026-09-26T21:25:09Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Per-book results and any detected sync conflicts are carried in
|
||||||
|
`book_results` and `conflicts` when present.
|
||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
| Code | Description |
|
| Code | Description |
|
||||||
| ---- | ---------------------------- |
|
| ---- | ----------- |
|
||||||
|
| 400 | Invalid request format |
|
||||||
| 401 | Device authentication failed |
|
| 401 | Device authentication failed |
|
||||||
| 400 | Invalid request data |
|
| 500 | Database error |
|
||||||
| 404 | Device not found |
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Delete Media Item Progress
|
||||||
|
|
||||||
|
Delete the stored reading progress for a media item.
|
||||||
|
|
||||||
|
**Endpoint**: `DELETE /api/media-items/:id/progress`
|
||||||
|
**Auth**: Required (Bearer token)
|
||||||
|
|
||||||
|
## Path Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ------------- | -------- | ---------------- |
|
||||||
|
| id | string (UUID) | Yes | Media item UUID |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```http
|
||||||
|
DELETE /api/media-items/774641f9-317b-4087-8e04-53bb4392ae56/progress
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "reading progress deleted"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| ---- | ----------- |
|
||||||
|
| 400 | Invalid media item id |
|
||||||
|
| 401 | Invalid or expired token |
|
||||||
|
| 500 | Database error |
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Delete Reading Progress
|
|
||||||
|
|
||||||
Delete reading progress for a media item.
|
|
||||||
|
|
||||||
**Endpoint**: `DELETE /api/media-items/{media_id}/progress`
|
|
||||||
**Auth**: Required
|
|
||||||
|
|
||||||
## Path Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ------ | -------- | --------------- |
|
|
||||||
| media_id | string | Yes | Media item UUID |
|
|
||||||
|
|
||||||
## Request Headers
|
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
|
||||||
| ------------- | ------ | -------- | ------------ |
|
|
||||||
| Authorization | string | Yes | Bearer token |
|
|
||||||
|
|
||||||
### Example Request
|
|
||||||
|
|
||||||
```http
|
|
||||||
DELETE /api/media-items/uuid/progress
|
|
||||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Response (204 No Content)
|
|
||||||
|
|
||||||
Progress deleted successfully.
|
|
||||||
|
|
||||||
## Error Responses
|
|
||||||
|
|
||||||
| Code | Description |
|
|
||||||
| ---- | ------------------------ |
|
|
||||||
| 401 | Invalid or expired token |
|
|
||||||
| 404 | Media item not found |
|
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Get Media Item Progress
|
||||||
|
|
||||||
|
Get the stored reading progress for a media item, plus the
|
||||||
|
server-derived restore handles for reflowable books.
|
||||||
|
|
||||||
|
**Endpoint**: `GET /api/media-items/:id/progress`
|
||||||
|
**Auth**: Required (Bearer token)
|
||||||
|
|
||||||
|
See [Position Contract](position-contract.md) for the semantics of every
|
||||||
|
field — what is verified, what the currencies are, and how clients
|
||||||
|
should restore.
|
||||||
|
|
||||||
|
## Path Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ------------- | -------- | --------------- |
|
||||||
|
| id | string (UUID) | Yes | Media item UUID |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/media-items/774641f9-317b-4087-8e04-53bb4392ae56/progress
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response (200 OK) — no progress stored
|
||||||
|
|
||||||
|
When the item has no progress row, an empty fixed-layout-style stub is
|
||||||
|
returned (not 404):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"current_page": 0,
|
||||||
|
"total_pages": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response (200 OK) — progress stored
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "e8239659-0ed6-42ae-a946-8440d7655b42",
|
||||||
|
"media_item_id": "774641f9-317b-4087-8e04-53bb4392ae56",
|
||||||
|
"user_id": "1b64992e-3408-4d84-9e03-e2dc4950e1dd",
|
||||||
|
"current_page": null,
|
||||||
|
"total_pages": null,
|
||||||
|
"last_read_at": "2026-09-26T14:52:34.806446Z",
|
||||||
|
"percentage": 0.045,
|
||||||
|
"character_offset": 16375,
|
||||||
|
"epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/58/1:598)",
|
||||||
|
"chapter": null,
|
||||||
|
"chapter_progress": null,
|
||||||
|
"format_group": "reflowable",
|
||||||
|
"total_characters": 592216,
|
||||||
|
"chapter_count": 1,
|
||||||
|
"last_sync_device": "web",
|
||||||
|
"last_sync_source": "koreader",
|
||||||
|
"last_sync_timestamp": "2026-09-26T14:52:34.806446Z",
|
||||||
|
"css_selector": "body>div:nth-child(4)>p:nth-child(29)",
|
||||||
|
"anchor_href": "1984.xhtml",
|
||||||
|
"char_offset": 598,
|
||||||
|
"context_text": "from day to day, but there was none in which Goldstein was not the principal figure. He was the prim"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Field Reference
|
||||||
|
|
||||||
|
Base fields (always present when a row exists):
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
| ----- | ---- | ----------- |
|
||||||
|
| `percentage` | float | Position as a fraction of the whole book (0..1). |
|
||||||
|
| `epubcfi` | string | The stored canonical standard CFI. **Spine steps index the OPF spine as written, including `linear="no"` items — do not resolve them against a readium reading order.** See the [Position Contract](position-contract.md#spine-numbering-hazard--read-this-before-parsing-a-stored-cfi). |
|
||||||
|
| `context_text` | string | The stored verification context (≤100 whitespace-normalized chars from the anchor). |
|
||||||
|
| `character_offset` | int | Book-wide rune offset. Internal currency — consistent with `total_characters`. |
|
||||||
|
| `current_page`, `total_pages` | int | Fixed-layout page position (null for reflowable). |
|
||||||
|
| `chapter`, `chapter_progress` | int, float | Chapter index and within-chapter fraction, when known. |
|
||||||
|
| `format_group` | string | `reflowable`, `fixed_layout`, `comic_archive`, … Gates the restore handles. |
|
||||||
|
| `total_characters`, `chapter_count` | int | Book metrics, for client-side fraction math. |
|
||||||
|
| `last_sync_device`, `last_sync_source`, `last_sync_timestamp` | — | Which client last wrote the row. |
|
||||||
|
|
||||||
|
Restore handles (conditional — served only for convertible reflowable
|
||||||
|
books whose stored anchor re-resolves at GET time):
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
| ----- | ---- | ----------- |
|
||||||
|
| `anchor_href` | string | The spine document containing the anchor (e.g. `1984.xhtml`). **Resolve your resource by this**, never by the CFI's spine step. |
|
||||||
|
| `css_selector` | string | Body-relative chain to the anchor's block element. |
|
||||||
|
| `char_offset` | int | Anchor offset within the block's concatenated text, in UTF-16 code units. |
|
||||||
|
| `epubcfi` | string | Re-served (possibly healed) canonical CFI — present whenever re-verification produced one. |
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| ---- | ----------- |
|
||||||
|
| 400 | Invalid media item id |
|
||||||
|
| 401 | Invalid or expired token |
|
||||||
|
| 500 | Database error |
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# Get Reading Progress
|
|
||||||
|
|
||||||
Retrieve reading progress for a specific media item.
|
|
||||||
|
|
||||||
**Endpoint**: `GET /api/media-items/{media_id}/progress`
|
|
||||||
**Auth**: Required
|
|
||||||
|
|
||||||
## Path Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ------ | -------- | --------------- |
|
|
||||||
| media_id | string | Yes | Media item UUID |
|
|
||||||
|
|
||||||
## Request Headers
|
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
|
||||||
| ------------- | ------ | -------- | ------------ |
|
|
||||||
| Authorization | string | Yes | Bearer token |
|
|
||||||
|
|
||||||
### Example Request
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/media-items/uuid/progress
|
|
||||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Response (200 OK)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"media_item_id": "uuid",
|
|
||||||
"user_id": "uuid",
|
|
||||||
"current_page": 45,
|
|
||||||
"total_pages": 200,
|
|
||||||
"percentage": 0.225,
|
|
||||||
"character_offset": 15432,
|
|
||||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
|
||||||
"chapter": 3,
|
|
||||||
"chapter_progress": 0.5,
|
|
||||||
"last_read_at": "2026-01-31T10:00:00Z",
|
|
||||||
"format_group": "reflowable",
|
|
||||||
"viewport_y": 0.12,
|
|
||||||
"zoom_level": 1.0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Responses
|
|
||||||
|
|
||||||
| Code | Description |
|
|
||||||
| ---- | ------------------------ |
|
|
||||||
| 401 | Invalid or expired token |
|
|
||||||
| 404 | Media item not found |
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
# Get Universal Progress
|
|
||||||
|
|
||||||
Get universal (device-agnostic) reading progress for a media item.
|
|
||||||
|
|
||||||
**Endpoint**: `GET /api/progress/:id`
|
|
||||||
**Auth**: Required
|
|
||||||
|
|
||||||
## Path Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ------------- | -------- | --------------- |
|
|
||||||
| id | string (UUID) | Yes | Media item UUID |
|
|
||||||
|
|
||||||
## Request Headers
|
|
||||||
|
|
||||||
| Header | Type | Required | Description |
|
|
||||||
| ------------- | ------ | -------- | ------------ |
|
|
||||||
| Authorization | string | Yes | Bearer token |
|
|
||||||
|
|
||||||
### Example Request
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/progress/550e8400-e29b-41d4-a716-446655440000
|
|
||||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Response (200 OK)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"media_item_id": "uuid",
|
|
||||||
"percentage": 75.5,
|
|
||||||
"position": 1234,
|
|
||||||
"page": 150,
|
|
||||||
"total_pages": 200,
|
|
||||||
"finished": false,
|
|
||||||
"updated_at": "2026-02-08T10:00:00Z",
|
|
||||||
"device": {
|
|
||||||
"id": "device-uuid",
|
|
||||||
"name": "My Kobo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Responses
|
|
||||||
|
|
||||||
| Code | Description |
|
|
||||||
| ---- | ------------------------ |
|
|
||||||
| 401 | Invalid or expired token |
|
|
||||||
| 404 | Media item not found |
|
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Position Contract
|
||||||
|
|
||||||
|
How reading positions are represented, submitted, verified, and restored
|
||||||
|
across clients. This is the contract every client speaks — readium-based
|
||||||
|
apps, web (foliate), KOReader, and Kobo devices alike.
|
||||||
|
|
||||||
|
**The server is the position authority.** Every submission is
|
||||||
|
independently verified against the book itself before it is stored; a
|
||||||
|
client whose locator math is wrong cannot poison the stored position.
|
||||||
|
In exchange, the server hands back handles clients can apply directly,
|
||||||
|
so they never have to parse or trust CFIs themselves.
|
||||||
|
|
||||||
|
## The three-tier submission
|
||||||
|
|
||||||
|
Clients submit what their renderer can reliably observe. All three tiers
|
||||||
|
are accepted on `PUT /api/media-items/:id/progress` (and on the KOReader
|
||||||
|
device endpoints):
|
||||||
|
|
||||||
|
| Tier | Field | Currency | Notes |
|
||||||
|
| ------ | -------------- | ----------------------------------------------------- | ----- |
|
||||||
|
| 1 | `percentage` | Float 0..1 of the whole book | Universal. The only field every client can supply. |
|
||||||
|
| 2 | `context_text` | Up to 100 chars, whitespace-normalized, starting at the anchor | The verification anchor. The server extracts the text at the submitted structural anchor and compares. |
|
||||||
|
| 3 | `epubcfi` | Standard wrapped CFI, terminals in UTF-16 code units | Optional structural anchor. KOReader submits a CRE xpointer and the server converts it. |
|
||||||
|
|
||||||
|
Tier 2 is what makes the system self-correcting: percentages alone
|
||||||
|
cannot distinguish "the reader's locator is right" from "the reader
|
||||||
|
silently reported wherever it is currently scrolled".
|
||||||
|
|
||||||
|
## Verification and healing (ingest side)
|
||||||
|
|
||||||
|
On every progress save for a convertible reflowable book that carries a
|
||||||
|
`context_text`, the server:
|
||||||
|
|
||||||
|
1. Parses the submitted `epubcfi` (if any) and resolves it against the
|
||||||
|
book's own XHTML.
|
||||||
|
2. Extracts the text at the resolved anchor and cross-checks it with the
|
||||||
|
submitted `context_text`.
|
||||||
|
3. On a mismatch — or an unresolvable anchor, or no anchor at all —
|
||||||
|
heals the position by text search, using the submitted `percentage`
|
||||||
|
to disambiguate repeated phrases. The healed CFI and a recomputed
|
||||||
|
percentage are stored in place of the submitted ones.
|
||||||
|
4. Refreshes the book-wide `character_offset` column from the verified
|
||||||
|
anchor on every verified save, so it never goes stale behind the
|
||||||
|
anchor.
|
||||||
|
|
||||||
|
If verification fails outright (book file unreadable, context not found
|
||||||
|
and percentage cannot disambiguate), the error is logged and the
|
||||||
|
submission is stored as-is — verification never rejects a save, it only
|
||||||
|
corrects.
|
||||||
|
|
||||||
|
## The restore handles
|
||||||
|
|
||||||
|
`GET /api/media-items/:id/progress` serves, alongside the raw stored
|
||||||
|
fields, the server-derived handles for reflowable books with a
|
||||||
|
resolvable anchor:
|
||||||
|
|
||||||
|
| Field | Currency | Meaning |
|
||||||
|
| ------------- | ----------------------- | ------- |
|
||||||
|
| `anchor_href` | — | The spine document the anchor lives in (e.g. `1984.xhtml`). **This is how a client finds the right resource.** |
|
||||||
|
| `css_selector`| — | Body-relative `tag:nth-child(k)` chain of the anchor's block element (e.g. `body>div:nth-child(4)>p:nth-child(29)`). |
|
||||||
|
| `char_offset` | UTF-16 code units | Offset of the anchor within the concatenated text of that block. |
|
||||||
|
| `epubcfi` | UTF-16 terminals | The stored canonical CFI — re-served healed if the GET-time re-verification improved it. |
|
||||||
|
| `context_text`| — | The stored verification context (≤100 normalized chars from the anchor). |
|
||||||
|
|
||||||
|
**Recommended restore sequence for a client:**
|
||||||
|
|
||||||
|
1. Open the book and jump to the stored `percentage` (coarse floor).
|
||||||
|
2. Resolve `anchor_href` against your own spine (an ends-with match on
|
||||||
|
document hrefs) and open that document if you are not already there.
|
||||||
|
3. Query `css_selector` in that document, walk its text nodes counting
|
||||||
|
UTF-16 units to `char_offset`, and scroll that position into view.
|
||||||
|
4. If the anchor cannot be measured (renderer-specific laziness), the
|
||||||
|
percentage floor stands.
|
||||||
|
|
||||||
|
## SPINE NUMBERING HAZARD — read this before parsing a stored CFI
|
||||||
|
|
||||||
|
**`epubcfi` spine steps index the OPF spine AS WRITTEN, including
|
||||||
|
`linear="no"` items.** Several rendering engines (notably readium)
|
||||||
|
number their reading order EXCLUDING `linear="no"` items. When a book's
|
||||||
|
cover (or any other item) is `linear="no"`, the two numberings differ by
|
||||||
|
a constant offset from that item onward — a client resolving a stored
|
||||||
|
CFI's spine step against its own numbering lands in the WRONG DOCUMENT.
|
||||||
|
|
||||||
|
This is not hypothetical: "1984" epubs commonly have a `linear="no"`
|
||||||
|
cover, which makes readium spine 0 = OPF spine 1. The server heals such
|
||||||
|
numbering mismatches at ingest (that is what the context check is for),
|
||||||
|
but the durable rule for client authors is:
|
||||||
|
|
||||||
|
> **Never resolve a stored CFI's spine step yourself.** Resolve the
|
||||||
|
> document by `anchor_href`, then land with `css_selector` +
|
||||||
|
> `char_offset`. Treat the CFI as opaque server currency.
|
||||||
|
|
||||||
|
## Offset currencies
|
||||||
|
|
||||||
|
Two counting systems are in play, and they are deliberately kept apart:
|
||||||
|
|
||||||
|
| Quantity | Currency | Why |
|
||||||
|
| -------- | -------- | --- |
|
||||||
|
| CFI terminal offsets (`…/1:456`) | UTF-16 code units | The EPUB CFI spec, and what every client observes (JavaScript `.length`). |
|
||||||
|
| `char_offset` (block-relative handle) | UTF-16 code units | Same reason — clients walk DOM text with JS semantics. |
|
||||||
|
| KOReader CRE `text().N` offsets | UTF-16 code units | crengine is UCS-16 internally. |
|
||||||
|
| `character_offset` (book-wide column) | Unicode runes | Internal, consistent with `total_characters` and the percentage derivations. |
|
||||||
|
|
||||||
|
For all-BMP text the two currencies are identical. They diverge on
|
||||||
|
astral-plane characters (emoji, rare CJK ideographs): one rune, two
|
||||||
|
UTF-16 units. The server converts at every wire boundary; internal
|
||||||
|
arithmetic never crosses.
|
||||||
|
|
||||||
|
## `context_text` rules
|
||||||
|
|
||||||
|
- Starts at the anchor position (it may begin mid-word).
|
||||||
|
- Whitespace-normalized (all runs of whitespace collapse to single
|
||||||
|
spaces).
|
||||||
|
- At most 100 characters.
|
||||||
|
- Comparison is containment-based (client and server suffixes of the
|
||||||
|
same block verify in either direction); contexts shorter than 12
|
||||||
|
chars never match.
|
||||||
|
|
||||||
|
## Engine-specific notes
|
||||||
|
|
||||||
|
- **readium-based clients** (the Android app): submit
|
||||||
|
percentage + `context_text` + a CFI generated from the laid-out
|
||||||
|
WebView. Their locally-generated CFI spine steps use readium
|
||||||
|
numbering — the server heals the difference; nothing to do.
|
||||||
|
- **Web (foliate)**: submit all three tiers; foliate CFIs are the same
|
||||||
|
currency the server stores.
|
||||||
|
- **KOReader**: submits a CRE xpointer in the `epubcfi` field of the
|
||||||
|
device payload (historical field name; it is a CRE xpointer, not a
|
||||||
|
CFI). The server converts CRE → canonical at ingest and canonical →
|
||||||
|
CRE on pull (`koreader_xpointer` in the metadata response).
|
||||||
|
- **Kobo**: submits kepub CFI locators, converted server-side the same
|
||||||
|
way.
|
||||||
|
- **Fixed-layout content** (PDF/CBZ): the page index is the canonical
|
||||||
|
locator; CFI/xpointer are meaningless and neither submitted nor
|
||||||
|
served.
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Update Media Item Progress
|
||||||
|
|
||||||
|
Submit reading progress for a media item. This is the position-authority
|
||||||
|
ingest point: the server independently verifies the submission against
|
||||||
|
the book and heals it when the client's projection is wrong.
|
||||||
|
|
||||||
|
**Endpoint**: `PUT /api/media-items/:id/progress`
|
||||||
|
**Auth**: Required (Bearer token)
|
||||||
|
**Content-Type**: `application/json`
|
||||||
|
|
||||||
|
See the [Position Contract](position-contract.md) for the three-tier
|
||||||
|
submission model, the verification/healing semantics, and the offset
|
||||||
|
currencies.
|
||||||
|
|
||||||
|
## Path Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ------------- | -------- | ---------------- |
|
||||||
|
| id | string (UUID) | Yes | Media item UUID |
|
||||||
|
|
||||||
|
## Request Body
|
||||||
|
|
||||||
|
All fields are optional; submit what your renderer can observe.
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
| ----- | ---- | ----------- |
|
||||||
|
| `percentage` | float | Position as a fraction of the whole book (0..1). Tier 1 — the universal field. |
|
||||||
|
| `context_text` | string | Up to 100 whitespace-normalized chars starting at the anchor. Tier 2 — enables verification and healing. |
|
||||||
|
| `epubcfi` | string | Standard wrapped CFI (UTF-16 terminals). Tier 3 — the structural anchor. Note: the server re-derives the stored canonical CFI; a client's own spine numbering is healed if it disagrees with the OPF spine. |
|
||||||
|
| `character_offset` | int | Book-wide rune offset. Accepted but recomputed server-side from the verified anchor on every verified save. |
|
||||||
|
| `current_page`, `total_pages` | int | Fixed-layout position. For fixed-layout formats the page index is the canonical locator. |
|
||||||
|
| `chapter`, `chapter_progress` | int, float | Chapter index and within-chapter fraction. |
|
||||||
|
| `reading_mode` | string | `paged` or `scrolled` (fixed-layout reader state). |
|
||||||
|
| `zoom_level`, `scroll_position_x`, `scroll_position_y` | float | Fixed-layout viewport state. |
|
||||||
|
|
||||||
|
### Example Request (reflowable, all three tiers)
|
||||||
|
|
||||||
|
```http
|
||||||
|
PUT /api/media-items/774641f9-317b-4087-8e04-53bb4392ae56/progress
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"percentage": 0.0415,
|
||||||
|
"context_text": "was at war with one of these Powers it was generally at peace with the other. But what was strange w",
|
||||||
|
"epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/62/1:456)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Request (fixed-layout)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"percentage": 0.15,
|
||||||
|
"current_page": 30,
|
||||||
|
"total_pages": 194,
|
||||||
|
"reading_mode": "paged"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
- The submission is verified against the book's XHTML when
|
||||||
|
`context_text` is present and the book is convertible reflowable: the
|
||||||
|
CFI is resolved, the text at the anchor is compared with
|
||||||
|
`context_text`, and mismatches heal by text search (percentage
|
||||||
|
disambiguates repeats). See [Position Contract](position-contract.md).
|
||||||
|
- **Anti-clobber guard**: a submission with `percentage < 0.005` is
|
||||||
|
ignored with `{"status": "ignored"}` when the stored row already holds
|
||||||
|
a percentage above `0.01` — re-opening a book at its first page does
|
||||||
|
not wipe real progress.
|
||||||
|
- The response is the stored row after verification, including the
|
||||||
|
healed `epubcfi` and the refreshed `character_offset` when
|
||||||
|
verification ran.
|
||||||
|
|
||||||
|
## Response (200 OK)
|
||||||
|
|
||||||
|
The saved progress row (same shape as
|
||||||
|
[GET](get_media_progress.md), minus the GET-time handles). The
|
||||||
|
`epubcfi` and `percentage` in the response are the server-verified
|
||||||
|
values, which may differ from the submitted ones when healing occurred.
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| ---- | ----------- |
|
||||||
|
| 400 | Invalid request data |
|
||||||
|
| 401 | Invalid or expired token |
|
||||||
|
| 500 | Database error |
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Update Reading Progress
|
|
||||||
|
|
||||||
Update reading progress for a media item. This will sync across all devices via WebSocket.
|
|
||||||
|
|
||||||
**Endpoint**: `PUT /api/media-items/{media_id}/progress`
|
|
||||||
**Auth**: Required
|
|
||||||
**Content-Type**: `application/json`
|
|
||||||
|
|
||||||
## Path Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ------ | -------- | --------------- |
|
|
||||||
| media_id | string | Yes | Media item UUID |
|
|
||||||
|
|
||||||
## Request Body
|
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
| --------------------------- | ------- | -------- | ------------------------------------------------- |
|
|
||||||
| source | string | Yes | Progress source (e.g., "web", "koreader", "kobo") |
|
|
||||||
| location | object | Yes | Location information |
|
|
||||||
| location.percentage | float | No | Progress percentage (0-1) |
|
|
||||||
| location.epubcfi | string | No | EPUB CFI location |
|
|
||||||
| location.character | integer | No | Character offset |
|
|
||||||
| location.chapter | integer | No | Chapter number |
|
|
||||||
| location.page | integer | No | Current page |
|
|
||||||
| location.total_pages | integer | No | Total pages |
|
|
||||||
| device_metadata | object | No | Device metadata |
|
|
||||||
| device_metadata.device_type | string | No | Device type |
|
|
||||||
| device_metadata.user_agent | string | No | User agent string |
|
|
||||||
|
|
||||||
### Example Request
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"source": "web",
|
|
||||||
"location": {
|
|
||||||
"percentage": 0.45678,
|
|
||||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
|
||||||
"character": 15432,
|
|
||||||
"chapter": 3,
|
|
||||||
"page": 89,
|
|
||||||
"total_pages": 200
|
|
||||||
},
|
|
||||||
"device_metadata": {
|
|
||||||
"device_type": "web",
|
|
||||||
"user_agent": "Mozilla/5.0..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Response (200 OK)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"sync_status": "success",
|
|
||||||
"progress_updated": true,
|
|
||||||
"devices_notified": ["device-1", "device-2"],
|
|
||||||
"broadcast": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Responses
|
|
||||||
|
|
||||||
| Code | Description |
|
|
||||||
| ---- | ------------------------ |
|
|
||||||
| 400 | Invalid location data |
|
|
||||||
| 401 | Invalid or expired token |
|
|
||||||
| 404 | Media item not found |
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# Update Universal Progress
|
|
||||||
|
|
||||||
Update universal reading progress for a media item.
|
|
||||||
|
|
||||||
**Endpoint**: `POST /api/progress/:id`
|
|
||||||
**Auth**: Required
|
|
||||||
**Content-Type**: `application/json`
|
|
||||||
|
|
||||||
## Path Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ------------- | -------- | --------------- |
|
|
||||||
| id | string (UUID) | Yes | Media item UUID |
|
|
||||||
|
|
||||||
## Request Body
|
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
| ---------- | ------------- | -------- | ------------------------------------------- |
|
|
||||||
| percentage | float | No | Progress percentage (0-100) |
|
|
||||||
| position | integer | No | Current position in bytes |
|
|
||||||
| page | integer | No | Current page number |
|
|
||||||
| finished | boolean | No | Whether the book is finished |
|
|
||||||
| device_id | string (UUID) | No | Device UUID (optional, for tracking source) |
|
|
||||||
|
|
||||||
### Example Request
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"percentage": 75.5,
|
|
||||||
"position": 1234,
|
|
||||||
"page": 150,
|
|
||||||
"finished": false,
|
|
||||||
"device_id": "device-uuid"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Response (200 OK)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"media_item_id": "uuid",
|
|
||||||
"percentage": 75.5,
|
|
||||||
"position": 1234,
|
|
||||||
"page": 150,
|
|
||||||
"finished": false,
|
|
||||||
"updated_at": "2026-02-08T10:00:00Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Responses
|
|
||||||
|
|
||||||
| Code | Description |
|
|
||||||
| ---- | ------------------------ |
|
|
||||||
| 400 | Invalid request data |
|
|
||||||
| 401 | Invalid or expired token |
|
|
||||||
| 404 | Media item not found |
|
|
||||||
Reference in New Issue
Block a user