docs: add Carousel dashboard implementation plan
This commit is contained in:
@@ -1,57 +0,0 @@
|
||||
meta {
|
||||
name: Get Admin Library
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/admin/library
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() !== 200) {
|
||||
bru.testFailed("Expected status 200, got " + res.getStatus());
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = res.getHeader("content-type");
|
||||
if (!contentType || !contentType.includes("text/html")) {
|
||||
bru.testFailed("Expected content-type to contain text/html, got " + contentType);
|
||||
return;
|
||||
}
|
||||
|
||||
bru.testPassed("Admin library page returned successfully");
|
||||
}
|
||||
onResponse(res);
|
||||
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Admin Library Page
|
||||
|
||||
Retrieves the admin library page for administrative access.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /admin/library
|
||||
|
||||
**Headers:**
|
||||
- `Authorization` (string): Bearer token
|
||||
|
||||
**Response:**
|
||||
- HTML content for the admin library page
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
meta {
|
||||
name: Get Admin Profile
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/admin/profile
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Admin Profile
|
||||
|
||||
Retrieves the admin profile information for administrative access.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /admin/profile
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Response:**
|
||||
- JSON object containing admin profile details
|
||||
- `id` (string): Admin user ID
|
||||
- `email` (string): Admin email
|
||||
- `username` (string): Admin username
|
||||
- `theme` (string): Theme preference
|
||||
- `first_name` (string): First name
|
||||
- `last_name` (string): Last name
|
||||
- `is_admin` (boolean): Admin status
|
||||
- `created_at` (string): Account creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (non-admin users)
|
||||
- 404: Profile not found
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
meta {
|
||||
name: Get Device Usage
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/analytics/device-usage
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
tests {
|
||||
test("status must be 200", function() {
|
||||
expect(res.status).to.eql(200);
|
||||
});
|
||||
|
||||
test("response has devices array", function() {
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body).to.have.property("devices");
|
||||
expect(body.devices).to.be.an("array");
|
||||
});
|
||||
|
||||
test("devices have required fields", function() {
|
||||
const body = JSON.parse(res.body);
|
||||
if (body.devices.length > 0) {
|
||||
expect(body.devices[0]).to.have.property("device_name");
|
||||
expect(body.devices[0]).to.have.property("device_type");
|
||||
expect(body.devices[0]).to.have.property("sync_count");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
Get usage statistics for all devices.
|
||||
|
||||
**Endpoint**: GET /api/analytics/device-usage
|
||||
**Auth**: Required (Bearer token)
|
||||
|
||||
## Response Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| devices | array | List of device usage statistics |
|
||||
| devices[].id | string | Device ID |
|
||||
| devices[].device_name | string | Device name |
|
||||
| devices[].device_type | string | Device type (kobo, kindle, koreader) |
|
||||
| devices[].sync_count | int | Number of sync operations |
|
||||
| devices[].last_sync | string | Last sync timestamp |
|
||||
| devices[].total_reading_minutes | int | Total reading time on device |
|
||||
| devices[].books_read | int | Number of books completed on device |
|
||||
|
||||
## Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"devices": [
|
||||
{
|
||||
"id": "789e4567-e89b-12d3-a456-426614174001",
|
||||
"device_name": "My Kobo Clara",
|
||||
"device_type": "kobo",
|
||||
"sync_count": 45,
|
||||
"last_sync": "2026-02-08T17:25:00Z",
|
||||
"total_reading_minutes": 1250,
|
||||
"books_read": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| 401 | Unauthorized |
|
||||
| 500 | Internal server error |
|
||||
|
||||
## Notes
|
||||
|
||||
- Only shows devices registered to the authenticated user
|
||||
- Devices are sorted by sync_count in descending order
|
||||
- Includes both active and inactive devices
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
meta {
|
||||
name: Get Popular Books
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/analytics/popular-books?limit=10
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
tests {
|
||||
test("status must be 200", function() {
|
||||
expect(res.status).to.eql(200);
|
||||
});
|
||||
|
||||
test("response has books array", function() {
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body).to.have.property("books");
|
||||
expect(body.books).to.be.an("array");
|
||||
});
|
||||
|
||||
test("books have required fields", function() {
|
||||
const body = JSON.parse(res.body);
|
||||
if (body.books.length > 0) {
|
||||
expect(body.books[0]).to.have.property("title");
|
||||
expect(body.books[0]).to.have.property("author");
|
||||
expect(body.books[0]).to.have.property("read_count");
|
||||
expect(body.books[0]).to.have.property("avg_completion");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
Get popular books sorted by read count.
|
||||
|
||||
**Endpoint**: GET /api/analytics/popular-books
|
||||
**Auth**: Required (Bearer token)
|
||||
|
||||
## Query Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|-----------|-------------|
|
||||
| limit | int | No | Maximum number of books to return (default: 10) |
|
||||
|
||||
## Response Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| books | array | List of popular books |
|
||||
| books[].media_item_id | string | Book ID |
|
||||
| books[].title | string | Book title |
|
||||
| books[].author | string | Book author |
|
||||
| books[].read_count | int | Number of times read |
|
||||
| books[].avg_completion | float | Average completion rate (0-1) |
|
||||
| books[].cover_url | string | Cover image URL |
|
||||
|
||||
## Example Request
|
||||
|
||||
```
|
||||
GET /api/analytics/popular-books?limit=10
|
||||
```
|
||||
|
||||
## Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"books": [
|
||||
{
|
||||
"media_item_id": "323e4567-e89b-12d3-a456-426614174002",
|
||||
"title": "The Great Gatsby",
|
||||
"author": "F. Scott Fitzgerald",
|
||||
"read_count": 5,
|
||||
"avg_completion": 0.85,
|
||||
"cover_url": "/api/books/323e4567-e89b-12d3-a456-426614174002/cover"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| 401 | Unauthorized |
|
||||
| 500 | Internal server error |
|
||||
|
||||
## Notes
|
||||
|
||||
- Books are sorted by read_count in descending order
|
||||
- Only books owned by the authenticated user are included
|
||||
- avg_completion is calculated from all reading sessions
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
meta {
|
||||
name: Get Reading Stats Date Range
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/analytics/reading-stats?start_date=2024-01-01&end_date=2024-01-31
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
tests {
|
||||
test("status must be 200", function() {
|
||||
expect(res.status).to.eql(200);
|
||||
});
|
||||
|
||||
test("response has daily_reading_minutes array", function() {
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body).to.have.property("daily_reading_minutes");
|
||||
expect(body.daily_reading_minutes).to.be.an("array");
|
||||
});
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
Get reading statistics for a specific date range.
|
||||
|
||||
**Endpoint**: GET /api/analytics/reading-stats
|
||||
**Auth**: Required (Bearer token)
|
||||
|
||||
## Query Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|-----------|-------------|
|
||||
| start_date | string | No | Start date (ISO 8601 format, default: 30 days ago) |
|
||||
| end_date | string | No | End date (ISO 8601 format, default: today) |
|
||||
|
||||
## Response Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| total_books_read | int | Total books completed in range |
|
||||
| total_pages_read | int | Total pages read in range |
|
||||
| total_reading_time_minutes | int | Total reading time in minutes |
|
||||
| completion_rate | float | Percentage of books completed (0-1) |
|
||||
| daily_reading_minutes | array | Daily reading time per day |
|
||||
| daily_reading_minutes[].date | string | Date (ISO 8601) |
|
||||
| daily_reading_minutes[].minutes | int | Minutes read on that date |
|
||||
|
||||
## Example Request
|
||||
|
||||
```
|
||||
GET /api/analytics/reading-stats?start_date=2024-01-01&end_date=2024-01-31
|
||||
```
|
||||
|
||||
## Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"total_books_read": 2,
|
||||
"total_pages_read": 450,
|
||||
"total_reading_time_minutes": 720,
|
||||
"completion_rate": 0.85,
|
||||
"daily_reading_minutes": [
|
||||
{
|
||||
"date": "2024-01-01",
|
||||
"minutes": 30
|
||||
},
|
||||
{
|
||||
"date": "2024-01-02",
|
||||
"minutes": 45
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| 400 | Invalid date format |
|
||||
| 401 | Unauthorized |
|
||||
| 500 | Internal server error |
|
||||
|
||||
## Notes
|
||||
|
||||
- Dates must be in ISO 8601 format (YYYY-MM-DD)
|
||||
- The range is inclusive of both start and end dates
|
||||
- Daily data only includes days with reading activity > 0
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
meta {
|
||||
name: Get Reading Stats
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/analytics/reading-stats
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
tests {
|
||||
test("status must be 200", function() {
|
||||
expect(res.status).to.eql(200);
|
||||
});
|
||||
|
||||
test("response has required stats fields", function() {
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body).to.have.property("total_books_read");
|
||||
expect(body).to.have.property("total_pages_read");
|
||||
expect(body).to.have.property("total_reading_time_minutes");
|
||||
expect(body).to.have.property("completion_rate");
|
||||
expect(body).to.have.property("daily_reading_minutes");
|
||||
});
|
||||
|
||||
test("daily_reading_minutes is an array", function() {
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.daily_reading_minutes).to.be.an("array");
|
||||
});
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
Get overall reading statistics for the authenticated user.
|
||||
|
||||
**Endpoint**: GET /api/analytics/reading-stats
|
||||
**Auth**: Required (Bearer token)
|
||||
|
||||
## Query Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|-----------|-------------|
|
||||
| start_date | string | No | Start date (ISO 8601 format) |
|
||||
| end_date | string | No | End date (ISO 8601 format) |
|
||||
|
||||
## Response Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| total_books_read | int | Total books completed |
|
||||
| total_pages_read | int | Total pages read |
|
||||
| total_reading_time_minutes | int | Total reading time in minutes |
|
||||
| completion_rate | float | Average book completion rate (0-1) |
|
||||
| daily_reading_minutes | array | Daily reading time breakdown |
|
||||
| daily_reading_minutes[].date | string | Date (ISO 8601) |
|
||||
| daily_reading_minutes[].minutes | int | Minutes read on that date |
|
||||
|
||||
## Example Request
|
||||
|
||||
```
|
||||
GET /api/analytics/reading-stats
|
||||
```
|
||||
|
||||
## Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"total_books_read": 12,
|
||||
"total_pages_read": 3450,
|
||||
"total_reading_time_minutes": 5400,
|
||||
"completion_rate": 0.78,
|
||||
"daily_reading_minutes": [
|
||||
{
|
||||
"date": "2026-01-15",
|
||||
"minutes": 45
|
||||
},
|
||||
{
|
||||
"date": "2026-01-16",
|
||||
"minutes": 60
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| 400 | Invalid date format |
|
||||
| 401 | Unauthorized |
|
||||
| 500 | Internal server error |
|
||||
|
||||
## Notes
|
||||
|
||||
- Without date parameters, returns stats for the last 30 days
|
||||
- Dates must be in ISO 8601 format (YYYY-MM-DD) when provided
|
||||
- Only includes reading activity from the authenticated user
|
||||
- Daily data includes all days with reading activity
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
meta {
|
||||
name: Bulk Delete Media Items
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/media-items/bulk-delete
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"media_item_ids": [
|
||||
"{{bookId1}}",
|
||||
"{{bookId2}}",
|
||||
"{{bookId3}}"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
tests['Bulk delete successful'] = true;
|
||||
const body = res.getBody();
|
||||
tests['Has results array'] = Array.isArray(body.results);
|
||||
tests['Has total count'] = body.total !== undefined;
|
||||
tests['Has deleted count'] = body.deleted !== undefined;
|
||||
tests['Has failed count'] = body.failed !== undefined;
|
||||
} else {
|
||||
tests['Bulk delete failed'] = false;
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Bulk Delete Media Items
|
||||
|
||||
Deletes multiple media items in a single request.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/media-items/bulk-delete
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Request Body:**
|
||||
- `media_item_ids` (array of strings): Array of media item UUIDs to delete
|
||||
|
||||
**Response:**
|
||||
- `results` (array): Results for each deletion attempt
|
||||
- `total` (number): Total number of media items processed
|
||||
- `deleted` (number): Number of successfully deleted media items
|
||||
- `failed` (number): Number of failed deletions
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success (with partial results if some failed)
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden
|
||||
- 500: Internal server error
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"media_item_ids": [
|
||||
"uuid-1",
|
||||
"uuid-2",
|
||||
"uuid-3"
|
||||
]
|
||||
}
|
||||
```
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
meta {
|
||||
name: Bulk Update Media Items
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/media-items/bulk-update
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"media_item_updates": [
|
||||
{
|
||||
"media_item_id": "{{bookId1}}",
|
||||
"updates": {
|
||||
"title": "Updated Title",
|
||||
"genre": "Science Fiction",
|
||||
"tags": ["science fiction", "non-fiction", "ACME CORP."]
|
||||
}
|
||||
},
|
||||
{
|
||||
"media_item_id": "{{bookId2}}",
|
||||
"updates": {
|
||||
"author": "Updated Author",
|
||||
"contributors": ["O'Reilly Media", "Penguin Random House"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
tests['Bulk update successful'] = true;
|
||||
const body = res.getBody();
|
||||
tests['Has results array'] = Array.isArray(body.results);
|
||||
tests['Has total count'] = body.total !== undefined;
|
||||
tests['Has updated count'] = body.updated !== undefined;
|
||||
} else {
|
||||
tests['Bulk update failed'] = false;
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Bulk Update Media Items
|
||||
|
||||
Updates multiple media items in a single request with different fields for each item.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/media-items/bulk-update
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Request Body:**
|
||||
- `media_item_updates` (array): Array of update objects
|
||||
- `media_item_id` (string): Media item UUID to update
|
||||
- `updates` (object): Fields to update (can include title, author, genre, tags, contributors, etc.)
|
||||
|
||||
**Response:**
|
||||
- `results` (array): Results for each update attempt
|
||||
- `total` (number): Total number of media items processed
|
||||
- `updated` (number): Number of successfully updated media items
|
||||
- `failed` (number): Number of failed updates
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success (with partial results if some failed)
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden
|
||||
- 500: Internal server error
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"media_item_updates": [
|
||||
{
|
||||
"media_item_id": "uuid-1",
|
||||
"updates": {
|
||||
"title": "New Title",
|
||||
"genre": "Fiction",
|
||||
"tags": ["fiction", "adventure"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"media_item_id": "uuid-2",
|
||||
"updates": {
|
||||
"author": "Jane Doe",
|
||||
"contributors": ["Publisher Inc."]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Each media item can have different fields updated. Only the specified fields are modified for each item.
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "Bookhoard API",
|
||||
"type": "collection",
|
||||
"docs": "API test collection for Bookhoard. Test credentials and data are documented in TEST_DATA.md to maintain alignment with Go integration tests."
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
auth {
|
||||
mode: bearer
|
||||
}
|
||||
|
||||
|
||||
docs {
|
||||
# Bruno API Tests for Bookhoard
|
||||
This directory contains Bruno collection for testing the Bookhoard API with comprehensive REST documentation.
|
||||
Setup
|
||||
1. Install Bruno: https://www.usebruno.com/
|
||||
2. Open Bruno and import this collection folder
|
||||
3. Select the "localhost" environment
|
||||
4. Start the application with `podman-compose up --build` or `docker-compose up --build`
|
||||
5. Register/Login first, then use Bearer token for protected endpoints
|
||||
Available Tests
|
||||
Authentication (Public & Private)
|
||||
- **Register User**: POST /api/auth/register - Create new account with role-based restrictions
|
||||
- **Login User**: POST /api/auth/login - Authenticate (email or username)
|
||||
- **Refresh Token**: POST /api/auth/refresh - Get new access token
|
||||
- **Logout**: POST /api/auth/logout - Invalidate refresh token
|
||||
User Profile Management
|
||||
- **Get Profile**: GET /api/auth/profile - Get current user info
|
||||
- **Update Profile**: PUT /api/auth/profile - Update first_name, last_name
|
||||
- **Update Email**: PUT /api/auth/email - Update email address
|
||||
- **Update Username**: PUT /api/auth/username - Update username
|
||||
- **Update Password**: PUT /api/auth/password - Update password
|
||||
- **Update Theme**: PUT /api/auth/theme - Update theme preference
|
||||
Admin User Management
|
||||
- **List Users**: GET /api/auth/users - Get all users with complete info (admin only)
|
||||
- **Delete Account**: DELETE /api/auth/account - Delete own account or admin deletes other accounts
|
||||
Libraries (Admin Only)
|
||||
- **Create Library**: POST /api/libraries - Create new library (Ebooks, Comics, Manga)
|
||||
- **Get Libraries**: GET /api/libraries - List all libraries (admin)
|
||||
- **Get Library**: GET /api/libraries/:id - Get library details
|
||||
- **Update Library**: PUT /api/libraries/:id - Update library settings
|
||||
- **Delete Library**: DELETE /api/libraries/:id - Remove library
|
||||
- **Add Library Folder**: POST /api/libraries/:id/folders - Add scanning folder
|
||||
- **Get Library Folders**: GET /api/libraries/:id/folders - List folders
|
||||
- **Delete Library Folder**: DELETE /api/libraries/:id/folders/:folder_id - Remove folder
|
||||
- **Get Library Stats**: GET /api/libraries/:id/stats - Library statistics
|
||||
- **Get Library Types**: GET /api/libraries/types - Available library types
|
||||
Media Items (Mixed Access)
|
||||
- **List Media Items**: GET /api/media-items - Paginated list (filter/sort by library, author, series, etc.)
|
||||
- **Get Media Item**: GET /api/media-items/:id - Single item details (all users)
|
||||
- **Create Media Item**: POST /api/media-items - Add new item (admin only)
|
||||
- **Update Media Item**: PUT /api/media-items/:id - Modify metadata (admin only)
|
||||
- **Delete Media Item**: DELETE /api/media-items/:id - Remove item (admin only)
|
||||
- **Filter Media Items**: POST /api/media-items/filter - Advanced filtering
|
||||
- **EPUB Download**: GET /api/media-items/:id/download - Download EPUB file
|
||||
- **Cover Image**: GET /api/media-items/:id/cover - Get cover image
|
||||
Reading Progress (All Users)
|
||||
- **Get Progress**: GET /api/progress/:media_id - User's reading progress for media item
|
||||
- **Update Progress**: PUT /api/progress/:media_id - Update reading progress
|
||||
- **Get Device Progress**: GET /api/progress/device/:device_id - Progress by device
|
||||
Universal Progress (All Users)
|
||||
- **Get Universal Progress**: GET /api/universal-progress/:sha256 - Get progress by book hash
|
||||
- **Update Universal Progress**: PUT /api/universal-progress - Update universal progress
|
||||
Notes (All Users)
|
||||
- **Get Notes**: GET /api/notes/:media_id - Get notes for media item
|
||||
- **Create Note**: POST /api/notes - Add new note
|
||||
- **Update Note**: PUT /api/notes/:id - Update note content
|
||||
- **Delete Note**: DELETE /api/notes/:id - Remove note
|
||||
Highlights (All Users)
|
||||
- **Get Highlights**: GET /api/highlights/:media_id - Get highlights for media item
|
||||
- **Create Highlight**: POST /api/highlights - Add new highlight
|
||||
- **Update Highlight**: PUT /api/highlights/:id - Update highlight
|
||||
- **Delete Highlight**: DELETE /api/highlights/:id - Remove highlight
|
||||
Ratings (All Users)
|
||||
- **Get Rating**: GET /api/ratings/:media_id - User's rating (returns 0 if unrated)
|
||||
- **Create/Update Rating**: POST /api/ratings - Rate media item (1-5 stars, half-star precision)
|
||||
- **Delete Rating**: DELETE /api/ratings/:media_id - Remove rating
|
||||
Collections (All Users)
|
||||
- **List Collections**: GET /api/collections - Get user's collections
|
||||
- **Get Collection**: GET /api/collections/:id - Collection details with media items
|
||||
- **Create Collection**: POST /api/collections - Create new collection
|
||||
- **Update Collection**: PUT /api/collections/:id - Update collection
|
||||
- **Delete Collection**: DELETE /api/collections/:id - Remove collection
|
||||
- **Add Auto-Assign Rule**: POST /api/collections/:id/rules - Add automatic rule
|
||||
- **Remove Auto-Assign Rule**: DELETE /api/collections/:id/rules/:rule_id - Remove rule
|
||||
- **Test Rule**: POST /api/collections/:id/rules/test - Preview rule matches
|
||||
- **Bulk Assign**: POST /api/collections/:id/assign - Manually add media items
|
||||
Device Management (All Users)
|
||||
- **Register Device**: POST /api/devices/register - Register new device
|
||||
- **List Devices**: GET /api/devices - Get user's devices
|
||||
- **Get Device**: GET /api/devices/:id - Device details
|
||||
- **Delete Device**: DELETE /api/devices/:id - Unregister device
|
||||
- **Sync Device**: POST /api/devices/:id/sync - Trigger device sync
|
||||
Sync Protocols (Device Integration)
|
||||
- **KOReader Sync**: POST /api/sync/koreader - KOReader progress/notes/highlights sync
|
||||
- **Kobo Sync**: POST /api/sync/kobo - Kobo progress/notes/highlights sync
|
||||
Scanner (Admin Only)
|
||||
- **Scan Libraries**: POST /api/scanner/scan - Scan library folders
|
||||
- **Start Scanner**: POST /api/scanner/start - Start real-time monitoring
|
||||
- **Stop Scanner**: POST /api/scanner/stop - Stop monitoring
|
||||
- **Get Scan Settings**: GET /api/scanner/settings - Scan configuration
|
||||
Analytics (Admin Only)
|
||||
- **Get Analytics**: GET /api/analytics - Usage statistics and metrics
|
||||
Book Matching (All Users)
|
||||
- **Search Books**: GET /api/book-matching/search - Search by ISBN, title, author
|
||||
- **Link Book**: POST /api/book-matching/link - Link media item to external database
|
||||
OPDS (All Users)
|
||||
- **OPDS Feeds**: GET /opds/* - OPDS catalog for e-reader integration
|
||||
- **OPDS Acquisition**: GET /opds/acquisition/* - Download media items
|
||||
WebSocket (Real-time)
|
||||
- **WebSocket**: WS /api/ws - Real-time sync events (progress, notes, highlights)
|
||||
Collection Organization
|
||||
bruno/
|
||||
├── user/ # User authentication and profile
|
||||
│ ├── auth/ # Login, register, refresh
|
||||
│ ├── profile/ # Profile management
|
||||
│ └── admin/ # User administration (admin only)
|
||||
├── library/ # Library management
|
||||
│ ├── Create/Update/Delete Libraries
|
||||
│ ├── Library Folders
|
||||
│ ├── Library Stats
|
||||
│ └── Scan Settings
|
||||
├── media-items/ # Media item operations
|
||||
│ ├── List/Get/Create/Update/Delete
|
||||
│ ├── Filter and Sort
|
||||
│ ├── Download EPUB
|
||||
│ ├── Cover Images
|
||||
│ └── Ratings
|
||||
├── progress/ # Reading progress tracking
|
||||
├── universal-progress/ # Cross-device universal progress
|
||||
├── notes/ # User notes
|
||||
├── highlights/ # Book highlights
|
||||
├── collections/ # Smart collections
|
||||
├── devices/ # Device registration
|
||||
├── sync-koreader/ # KOReader sync protocol
|
||||
├── sync-kobo/ # Kobo sync protocol
|
||||
├── scanner/ # Library scanning
|
||||
├── analytics/ # Usage statistics
|
||||
├── books/ # Book matching/linking
|
||||
├── kobo/ # Kobo-specific operations
|
||||
├── koreader/ # KOReader-specific operations
|
||||
├── opds/ # OPDS catalog feeds
|
||||
└── admin/ # Admin operations
|
||||
## Security Features
|
||||
### Registration Restrictions
|
||||
- **First User**: Automatically gets admin role regardless of request
|
||||
- **Existing Admins**: Only authenticated admins can create new admin accounts
|
||||
- **Regular Users**: Anyone can create regular user accounts
|
||||
- **Unauthenticated**: Can only create first admin, not subsequent admins
|
||||
### User Management
|
||||
- **Self-Deletion**: Users can delete their own accounts
|
||||
- **Admin Override**: Admins can delete any user account
|
||||
- **Last Admin Protection**: Cannot delete the last admin account in the system
|
||||
### Role System
|
||||
- **Admin**: Full access - manage libraries, media items, users, scanner
|
||||
- **User**: Read access - view media items, create collections, track progress, rate, annotate
|
||||
### Device Authentication
|
||||
- **No Passwords**: Devices use QR code registration and access tokens
|
||||
- **User Approval**: Device registration requires user approval via web interface
|
||||
### JWT Tokens
|
||||
- **Access Token**: Valid for 1 hour, sent via Bearer header
|
||||
- **Refresh Token**: Valid for 7 days, used to get new access tokens
|
||||
### Rate Limiting
|
||||
- **Auth Endpoints**: 10 requests/minute per IP
|
||||
### Data Isolation
|
||||
- **Progress, Notes, Highlights, Ratings**: User-specific
|
||||
- **Collections**: User-specific (admins see all users' collections)
|
||||
- **Devices**: User-specific
|
||||
## Documentation Features
|
||||
Each request includes:
|
||||
- **Detailed descriptions** of functionality
|
||||
- **Parameter specifications** (required/optional, types)
|
||||
- **Request/Response examples**
|
||||
- **Error response codes** and meanings
|
||||
- **Authentication requirements**
|
||||
## Notes
|
||||
- **Authentication Flow**: Register → Login → Use Bearer token for all other requests
|
||||
- **Media Items vs Books**: The API uses "media items" (supports ebooks, comics, manga)
|
||||
- **Library System**: Organized by libraries (Ebooks, Comics, Manga) with scanning folders
|
||||
- **Universal Progress**: Cross-device sync using SHA-256 book hashes
|
||||
- **Smart Collections**: Auto-assign rules based on genre, author, series, tags, etc.
|
||||
- **OPDS Support**: Wireless book delivery to e-readers (Kobo, KOReader)
|
||||
- **Device Protocols**: Native sync for KOReader and Kobo devices
|
||||
- **Rating System**: Half-star precision (1-10 scale internally, displayed as 1-5 stars)
|
||||
- **Admin Setup**: First admin must be created by updating user role in database
|
||||
- **Variables**: Update collection variables for testing (media_id, library_id, device_id, etc.)
|
||||
- **Security**: Passwords hashed with bcrypt, unique email/username constraints, role-based access control
|
||||
- **JSON**: All requests/responses use JSON format
|
||||
- **WebSocket**: Real-time events for sync updates across devices
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
meta {
|
||||
name: Add Books to Collection
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/collections/{{collection_id}}/books
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
|
||||
body:json {
|
||||
{
|
||||
"book_ids": [
|
||||
"{{book_id_1}}",
|
||||
"{{book_id_2}}",
|
||||
"{{book_id_3}}"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
meta {
|
||||
name: Bulk Add Books to Collections
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/collections/bulk-add-books
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{authToken}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"operations": [
|
||||
{
|
||||
"collection_id": "{{collectionId1}}",
|
||||
"book_ids": [
|
||||
"{{bookId1}}",
|
||||
"{{bookId2}}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"collection_id": "{{collectionId2}}",
|
||||
"book_ids": [
|
||||
"{{bookId3}}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
tests['Bulk add successful'] = true;
|
||||
const body = res.getBody();
|
||||
tests['Has results array'] = Array.isArray(body.results);
|
||||
tests['All operations processed'] = body.results.length > 0;
|
||||
} else {
|
||||
tests['Bulk add failed'] = false;
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Bulk Add Books to Collections
|
||||
|
||||
Adds multiple books to multiple collections in a single request. Each operation specifies a collection and a list of books to add.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/collections/bulk-add-books
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `operations` (array): Array of collection-book operations
|
||||
- `collection_id` (string): Collection UUID
|
||||
- `book_ids` (array): Array of book UUIDs to add to the collection
|
||||
|
||||
**Response:**
|
||||
- `results` (array): Results for each operation
|
||||
- `total` (number): Total number of operations
|
||||
- `success` (number): Number of successful operations
|
||||
- `failed` (number): Number of failed operations
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success (with partial results if some failed)
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden
|
||||
- 500: Internal server error
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"operations": [
|
||||
{
|
||||
"collection_id": "collection-uuid-1",
|
||||
"book_ids": ["book-1", "book-2"]
|
||||
},
|
||||
{
|
||||
"collection_id": "collection-uuid-2",
|
||||
"book_ids": ["book-3"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Adding a book that's already in a collection is idempotent (no error).
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
meta {
|
||||
name: Bulk Remove Books - All Books
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/collections/{{collection_id}}/books/bulk-remove
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"book_ids": [
|
||||
"{{bookId1}}",
|
||||
"{{bookId2}}",
|
||||
"{{bookId3}}"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Bulk Remove Books from Collection
|
||||
|
||||
Removes multiple books from a collection in a single request.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/collections/{collection_id}/books/bulk-remove
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Path Parameters:**
|
||||
- `collection_id` (string): Collection UUID
|
||||
|
||||
**Request Body:**
|
||||
- `book_ids` (array): Array of book UUIDs to remove from the collection
|
||||
|
||||
**Response:**
|
||||
- `removed` (number): Number of books successfully removed
|
||||
- `total` (number): Total number of books processed
|
||||
- `results` (array): Results for each removal attempt
|
||||
- `book_id` (string): Book UUID
|
||||
- `success` (boolean): Whether the removal succeeded
|
||||
- `error` (string, optional): Error message if failed
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success (with partial results if some failed)
|
||||
- 400: Invalid request data (e.g., empty book_ids array)
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden
|
||||
- 404: Collection not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Example Request:**
|
||||
```json
|
||||
{
|
||||
"book_ids": [
|
||||
"book-uuid-1",
|
||||
"book-uuid-2",
|
||||
"book-uuid-3"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"removed": 2,
|
||||
"total": 3,
|
||||
"results": [
|
||||
{
|
||||
"book_id": "book-uuid-1",
|
||||
"success": true
|
||||
},
|
||||
{
|
||||
"book_id": "book-uuid-2",
|
||||
"success": true
|
||||
},
|
||||
{
|
||||
"book_id": "book-uuid-3",
|
||||
"success": false,
|
||||
"error": "Book not in collection"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Removing a book that's not in the collection returns success: false for that book but doesn't fail the entire request. Empty book_ids array returns 400.
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
meta {
|
||||
name: Bulk Remove Books - Empty List
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/collections/{{collection_id}}/books/bulk-remove
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{authToken}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"book_ids": []
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
tests['Returns 400 for empty list'] = res.getStatus() === 400;
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Bulk Remove Books - Empty List Validation
|
||||
|
||||
Tests validation behavior when providing an empty book_ids array.
|
||||
|
||||
**Expected Result:** 400 Bad Request
|
||||
|
||||
**Validation Rule:** book_ids array must contain at least one book UUID.
|
||||
|
||||
**Purpose:** Ensures the API properly validates input and rejects empty removal requests.
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
meta {
|
||||
name: Bulk Remove Books - Invalid IDs
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/collections/{{collection_id}}/books/bulk-remove
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{authToken}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"book_ids": [
|
||||
"{{bookId1}}",
|
||||
"invalid-uuid-format",
|
||||
"{{bookId2}}"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
tests['Partial success accepted'] = true;
|
||||
const body = res.getBody();
|
||||
tests['Has removed count'] = body.removed !== undefined;
|
||||
tests['Has results array'] = Array.isArray(body.results);
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Bulk Remove Books - Invalid IDs
|
||||
|
||||
Tests behavior when the book_ids array contains invalid UUID formats or non-existent books.
|
||||
|
||||
**Expected Result:** 200 OK with partial success
|
||||
|
||||
**Purpose:** Verifies that:
|
||||
- Invalid UUID formats don't crash the endpoint
|
||||
- Non-existent book IDs are handled gracefully
|
||||
- Valid IDs in the same request are still processed
|
||||
- Response includes detailed results showing which succeeded/failed
|
||||
|
||||
**Note:** The endpoint should process all valid IDs and report failures for invalid ones, allowing clients to handle partial failures appropriately.
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
meta {
|
||||
name: Bulk Remove Books - Single Book
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/collections/{{collection_id}}/books/bulk-remove
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{authToken}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"book_ids": [
|
||||
"{{bookId1}}"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
const body = res.getBody();
|
||||
tests['Single book removed'] = body.removed === 1;
|
||||
tests['Total is 1'] = body.total === 1;
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Bulk Remove Books - Single Book
|
||||
|
||||
Tests that bulk remove endpoint works correctly with a single book.
|
||||
|
||||
**Expected Result:** 200 OK with removed: 1, total: 1
|
||||
|
||||
**Purpose:** Verifies the bulk remove endpoint handles single-item arrays correctly, providing flexibility for clients to use the same endpoint for both single and multiple removals.
|
||||
|
||||
**Note:** Using bulk remove for a single book is functionally equivalent to the single remove endpoint but allows for consistent error handling and response format.
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
meta {
|
||||
name: Create Collection
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/collections
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
|
||||
body:json {
|
||||
{
|
||||
"name": "Science Fiction",
|
||||
"description": "My favorite sci-fi books",
|
||||
"color": "#ff0000",
|
||||
"icon": "🚀",
|
||||
"auto_assign_rules": [
|
||||
{
|
||||
"id": "rule-1",
|
||||
"field": "genre",
|
||||
"operator": "equals",
|
||||
"value": "Science Fiction",
|
||||
"priority": 8
|
||||
}
|
||||
],
|
||||
"view_settings": {
|
||||
"sort_by": "title",
|
||||
"view_mode": "grid"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
meta {
|
||||
name: Create Device Mapping
|
||||
type: http
|
||||
seq: 9
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/devices/{{device_id}}/collections
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
token: {{token}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"collection_id": "{{collection_id}}",
|
||||
"device_shelf_name": "Sci-Fi",
|
||||
"sync_direction": "bidirectional"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
meta {
|
||||
name: Delete Collection
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{base_url}}/api/collections/{{collection_id}}
|
||||
auth: inherit
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
meta {
|
||||
name: Delete Device Mapping
|
||||
type: http
|
||||
seq: 11
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{base_url}}/api/devices/{{device_id}}/collections/{{mapping_id}}
|
||||
auth: inherit
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
meta {
|
||||
name: Get Book Collections
|
||||
type: http
|
||||
seq: 12
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/collections/books/{{book_id}}
|
||||
auth: inherit
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
meta {
|
||||
name: Get Collection
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/collections/{{collection_id}}
|
||||
auth: inherit
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
meta {
|
||||
name: Get Collections
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/collections?include_auto=true&sort_by=name
|
||||
auth: inherit
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
meta {
|
||||
name: Get Device Mappings
|
||||
type: http
|
||||
seq: 8
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/devices/{{device_id}}/collections
|
||||
auth: inherit
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
meta {
|
||||
name: Remove Book from Collection
|
||||
type: http
|
||||
seq: 7
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{base_url}}/api/collections/{{collection_id}}/books/{{book_id}}
|
||||
auth: inherit
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
meta {
|
||||
name: Test Collection Rules - Author Contains
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/collections/test-rules
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{authToken}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"field": "author",
|
||||
"operator": "contains",
|
||||
"value": "Asimov"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
tests['Author search successful'] = true;
|
||||
const body = res.getBody();
|
||||
tests['Has matches array'] = Array.isArray(body.matches);
|
||||
tests['Found books by Asimov'] = body.matches.length > 0;
|
||||
} else {
|
||||
tests['Author search failed'] = false;
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Test Collection Rules - Author Contains
|
||||
|
||||
Tests the "contains" operator on the author field to find books by a specific author (partial match).
|
||||
|
||||
**Example Use Case:** Finding all books by an author whose name contains "Asimov" (e.g., "Isaac Asimov").
|
||||
|
||||
**Operator:** `contains` - Matches if the field contains the specified value as a substring (case-insensitive typically).
|
||||
|
||||
**Expected Result:** Returns all books where the author field contains "Asimov".
|
||||
|
||||
**Purpose:** Demonstrates text-based partial matching for author searches, useful when you don't need the exact author name or want to find books by authors with similar names.
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
meta {
|
||||
name: Test Collection Rules - Copyright Year Greater Than
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/collections/test-rules
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{authToken}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"field": "copyright_year",
|
||||
"operator": "greater_than",
|
||||
"value": "2000"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
tests['Year comparison successful'] = true;
|
||||
const body = res.getBody();
|
||||
tests['Has matches array'] = Array.isArray(body.matches);
|
||||
tests('Found books after 2000', body.matches.length >= 0);
|
||||
} else {
|
||||
tests['Year comparison failed'] = false;
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Test Collection Rules - Copyright Year Greater Than
|
||||
|
||||
Tests the "greater_than" operator on the copyright_year field to find books published after a specific year.
|
||||
|
||||
**Example Use Case:** Creating a "Modern Books" collection with books published after 2000.
|
||||
|
||||
**Operator:** `greater_than` - Matches if the field value is greater than the specified value (numeric comparison).
|
||||
|
||||
**Field:** `copyright_year` - The year the book was copyrighted/published.
|
||||
|
||||
**Expected Result:** Returns all books with copyright_year greater than 2000 (i.e., published in 2001 or later).
|
||||
|
||||
**Purpose:** Demonstrates numeric comparison operators for creating date-based collections, useful for organizing books by publication era.
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
meta {
|
||||
name: Test Collection Rules - Empty Rules Array
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/collections/test-rules
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{authToken}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"rules": []
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
tests['Empty rules rejected'] = res.getStatus() === 400;
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Test Collection Rules - Empty Rules Array
|
||||
|
||||
Tests validation behavior when providing an empty rules array.
|
||||
|
||||
**Expected Result:** 400 Bad Request
|
||||
|
||||
**Validation Rule:** rules array must contain at least one rule object.
|
||||
|
||||
**Purpose:** Ensures the API properly validates input and rejects empty rule sets, preventing accidental queries that would return all books or cause performance issues.
|
||||
|
||||
**Use Case:** Client-side validation should prevent sending empty rules, but the API should also validate to catch malformed requests.
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
meta {
|
||||
name: Test Collection Rules - No Matches
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/collections/test-rules
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{authToken}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"field": "genre",
|
||||
"operator": "equals",
|
||||
"value": "NonExistentGenre123456"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
const body = res.getBody();
|
||||
tests['No matches returned'] = body.total === 0;
|
||||
tests['Empty matches array'] = body.matches.length === 0;
|
||||
tests['Success with zero results'] = true;
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Test Collection Rules - No Matches
|
||||
|
||||
Tests behavior when collection rules don't match any books in the library.
|
||||
|
||||
**Example Use Case:** Validating that a new genre name doesn't exist before creating a collection for it, or testing edge cases.
|
||||
|
||||
**Expected Result:** 200 OK with empty matches array and total: 0
|
||||
|
||||
**Purpose:** Verifies that the API handles zero-match scenarios gracefully:
|
||||
- Returns 200 (success) not 404
|
||||
- Returns empty array, not null
|
||||
- Returns total: 0 for clarity
|
||||
- No errors thrown for no results
|
||||
|
||||
**Note:** An empty result set is a valid response and doesn't indicate an error. This allows users to test rules confidently before creating collections.
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
meta {
|
||||
name: Test Collection Rules
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/collections/test-rules
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{authToken}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"field": "genre",
|
||||
"operator": "equals",
|
||||
"value": "Science Fiction"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
tests['Rules test successful'] = true;
|
||||
const body = res.getBody();
|
||||
tests['Has matches array'] = Array.isArray(body.matches);
|
||||
tests['Has total count'] = body.total !== undefined;
|
||||
} else {
|
||||
tests['Rules test failed'] = false;
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Test Collection Rules
|
||||
|
||||
Tests collection rules against the library to see which books match, without creating a collection. Useful for previewing what books would be included in a collection with specific rules.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/collections/test-rules
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `rules` (array): Array of rule objects to test
|
||||
- `field` (string): Field to test (genre, author, copyright_year, tags, etc.)
|
||||
- `operator` (string): Comparison operator
|
||||
- `equals`: Exact match
|
||||
- `contains`: Contains substring (for text fields)
|
||||
- `greater_than`: Greater than (for numeric fields)
|
||||
- `less_than`: Less than (for numeric fields)
|
||||
- `not_equals`: Not equal to
|
||||
- `starts_with`: Starts with
|
||||
- `ends_with`: Ends with
|
||||
- `is_empty`: Field is empty or null
|
||||
- `is_not_empty`: Field is not empty and not null
|
||||
- `value` (string): Value to compare against (not required for is_empty/is_not_empty)
|
||||
|
||||
**Response:**
|
||||
- `matches` (array): Array of matching books
|
||||
- `id` (string): Book UUID
|
||||
- `title` (string): Book title
|
||||
- `author` (string): Book author
|
||||
- `genre` (string): Book genre
|
||||
- Additional book metadata
|
||||
- `total` (number): Total number of matching books
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success - returns matching books
|
||||
- 400: Invalid request (empty rules array, invalid field/operator)
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
|
||||
**Example Request:**
|
||||
```json
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"field": "genre",
|
||||
"operator": "equals",
|
||||
"value": "Science Fiction"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"id": "book-uuid-1",
|
||||
"title": "Foundation",
|
||||
"author": "Isaac Asimov",
|
||||
"genre": "Science Fiction"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** This endpoint is useful for validating collection rules before creating a collection, or for dynamically querying books based on criteria.
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
meta {
|
||||
name: Update Collection
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{base_url}}/api/collections/{{collection_id}}
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
|
||||
body:json {
|
||||
{
|
||||
"name": "Sci-Fi Favorites",
|
||||
"description": "Updated description",
|
||||
"color": "#00ff00",
|
||||
"icon": "⭐",
|
||||
"auto_assign_rules": [
|
||||
{
|
||||
"id": "rule-2",
|
||||
"field": "series",
|
||||
"operator": "equals",
|
||||
"value": "Foundation",
|
||||
"priority": 9
|
||||
}
|
||||
],
|
||||
"view_settings": {
|
||||
"sort_by": "author",
|
||||
"view_mode": "list"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
meta {
|
||||
name: Update Device Mapping
|
||||
type: http
|
||||
seq: 10
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{base_url}}/api/devices/{{device_id}}/collections/{{mapping_id}}
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
|
||||
body:json {
|
||||
{
|
||||
"device_shelf_name": "Science Fiction",
|
||||
"sync_direction": "book_to_device"
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
meta {
|
||||
name: Bulk Dismiss Conflicts
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/conflicts/bulk-dismiss
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{authToken}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"conflict_ids": [
|
||||
"{{conflictId1}}",
|
||||
"{{conflictId2}}"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
tests['Bulk dismiss successful'] = true;
|
||||
const body = res.getBody();
|
||||
tests['Has results array'] = Array.isArray(body.results);
|
||||
tests['Has total count'] = body.total !== undefined;
|
||||
tests['Has success count'] = body.success !== undefined;
|
||||
tests['Has failed count'] = body.failed !== undefined;
|
||||
} else {
|
||||
tests['Bulk dismiss failed'] = false;
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Bulk Dismiss Conflicts
|
||||
|
||||
Dismisses multiple sync conflicts without resolving them. This removes them from the conflict list while leaving the data unchanged.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/conflicts/bulk-dismiss
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `conflict_ids` (array): Array of conflict UUIDs to dismiss
|
||||
|
||||
**Response:**
|
||||
- `results` (array): Results for each dismissal
|
||||
- `total` (number): Total number of conflicts processed
|
||||
- `success` (number): Number of successfully dismissed conflicts
|
||||
- `failed` (number): Number of failed dismissals
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden
|
||||
- 404: One or more conflicts not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"conflict_ids": ["uuid-1", "uuid-2"]
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Dismissing a conflict removes it from the conflict list but does not merge or resolve the conflicting data. Use this when you want to ignore a conflict and handle it manually.
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
meta {
|
||||
name: Bulk Resolve Conflicts
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/conflicts/bulk-resolve
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{authToken}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"conflict_ids": [
|
||||
"{{conflictId1}}",
|
||||
"{{conflictId2}}",
|
||||
"{{conflictId3}}"
|
||||
],
|
||||
"strategy": "most_recent"
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
tests['Bulk resolve successful'] = true;
|
||||
const body = res.getBody();
|
||||
tests['Has results array'] = Array.isArray(body.results);
|
||||
tests['Has total count'] = body.total !== undefined;
|
||||
tests['Has success count'] = body.success !== undefined;
|
||||
tests['Has failed count'] = body.failed !== undefined;
|
||||
tests['Total equals sum of success and failed'] = body.total === body.success + body.failed;
|
||||
} else {
|
||||
tests['Bulk resolve failed'] = false;
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Bulk Resolve Conflicts
|
||||
|
||||
Resolves multiple sync conflicts in a single request using a specified resolution strategy.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/conflicts/bulk-resolve
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `conflict_ids` (array): Array of conflict UUIDs to resolve
|
||||
- `strategy` (string): Resolution strategy
|
||||
- `most_recent`: Use the most recently updated progress
|
||||
- `highest_progress`: Use the reading progress with the highest percent read
|
||||
- `server`: Always prefer server-side data
|
||||
- `device`: Always prefer device-side data
|
||||
|
||||
**Response:**
|
||||
- `results` (array): Results for each conflict resolution
|
||||
- `total` (number): Total number of conflicts processed
|
||||
- `success` (number): Number of successfully resolved conflicts
|
||||
- `failed` (number): Number of failed resolutions
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success (with partial results if some failed)
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden
|
||||
- 404: One or more conflicts not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"conflict_ids": ["uuid-1", "uuid-2", "uuid-3"],
|
||||
"strategy": "most_recent"
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Conflicts are resolved atomically per conflict. If one resolution fails, others may still succeed.
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
meta {
|
||||
name: Bulk Resolve with Highest Progress Strategy
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/conflicts/bulk-resolve
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{authToken}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"conflict_ids": [
|
||||
"{{conflictId1}}"
|
||||
],
|
||||
"strategy": "highest_progress"
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
tests['Highest progress strategy successful'] = true;
|
||||
const body = res.getBody();
|
||||
tests['At least one conflict resolved'] = body.success > 0;
|
||||
} else {
|
||||
tests['Strategy failed'] = false;
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Bulk Resolve with Highest Progress Strategy
|
||||
|
||||
Resolves multiple sync conflicts using the "highest_progress" strategy, which keeps the reading progress with the highest percentage read.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/conflicts/bulk-resolve
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `conflict_ids` (array): Array of conflict UUIDs to resolve
|
||||
- `strategy` (string): Must be "highest_progress"
|
||||
|
||||
**Response:**
|
||||
- `results` (array): Results for each conflict resolution
|
||||
- `total` (number): Total number of conflicts processed
|
||||
- `success` (number): Number of successfully resolved conflicts
|
||||
- `failed` (number): Number of failed resolutions
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden
|
||||
- 500: Internal server error
|
||||
|
||||
**Note:** The highest progress strategy is ideal when you want to preserve the most reading progress across devices. Use this when you've been reading on multiple devices and want to keep the furthest position.
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
meta {
|
||||
name: Delete Conflict
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{base_url}}/api/conflicts/{{conflict_id}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{token}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Delete Conflict
|
||||
|
||||
Permanently deletes a specific conflict record from the system.
|
||||
|
||||
**Method:** DELETE
|
||||
|
||||
**Endpoint:** /api/conflicts/{conflict_id}
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Path Parameters:**
|
||||
- `conflict_id` (string): Conflict UUID to delete
|
||||
|
||||
**Response:** 204 No Content on success
|
||||
|
||||
**Status Codes:**
|
||||
- 204: Success - conflict deleted
|
||||
- 401: Unauthorized
|
||||
- 404: Conflict not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Use Cases:**
|
||||
- Conflict was created in error
|
||||
- Dismissing a conflict without resolving it
|
||||
- Conflict is no longer relevant (e.g., book deleted)
|
||||
|
||||
**Note:** This permanently removes the conflict record with no undo option. Consider resolving the conflict instead if you want to maintain an audit trail of what happened.
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
meta {
|
||||
name: Dismiss All Resolved Conflicts
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/conflicts/dismiss-all
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{token}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Dismiss All Resolved Conflicts
|
||||
|
||||
Deletes all resolved conflicts for the authenticated user, cleaning up the conflict list.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/conflicts/dismiss-all
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Response:**
|
||||
- `deleted` (number): Number of conflict records that were deleted
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success - conflicts deleted
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"deleted": 5
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Clean up conflicts list after reviewing resolutions
|
||||
- Remove old resolved conflicts no longer needed
|
||||
- Maintain a clean conflict history
|
||||
|
||||
**Note:** Only conflicts with status "user_resolved" or "auto_resolved" are deleted. Unresolved conflicts are preserved.
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
meta {
|
||||
name: Get Conflict Details
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/conflicts/{{conflict_id}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{token}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Conflict Details
|
||||
|
||||
Retrieves detailed information about a specific conflict, including side-by-side comparison of conflicting data from all sources.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/conflicts/{conflict_id}
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Path Parameters:**
|
||||
- `conflict_id` (string): Conflict UUID
|
||||
|
||||
**Response:**
|
||||
- `id` (string): Conflict UUID
|
||||
- `media_item_id` (string): Associated book UUID
|
||||
- `media_item_title` (string): Book title
|
||||
- `conflict_type` (string): Type of conflict (progress, note, highlight)
|
||||
- `conflict_data` (object): Side-by-side comparison from each source
|
||||
- Each source includes:
|
||||
- `source` (string): Device/source identifier (koreader, kobo, web, etc.)
|
||||
- `timestamp` (string): When this data was recorded
|
||||
- `data` (object): The conflicting data
|
||||
- `percentage` (number): Reading progress
|
||||
- `epubcfi` (string): EPUB location
|
||||
- `chapter` (number): Chapter number
|
||||
- `resolution_status` (string): Current status (unresolved, user_resolved, auto_resolved)
|
||||
- `resolution_data` (object, optional): If resolved, includes resolution details
|
||||
- `resolved_by` (string, optional): User ID who resolved it
|
||||
- `resolved_at` (string, optional): When it was resolved
|
||||
- `created_at` (string): When conflict was detected
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 404: Conflict not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"id": "conflict-uuid",
|
||||
"media_item_id": "book-uuid",
|
||||
"media_item_title": "Foundation",
|
||||
"conflict_type": "progress",
|
||||
"conflict_data": {
|
||||
"koreader": {
|
||||
"source": "koreader",
|
||||
"timestamp": "2026-01-30T20:10:00Z",
|
||||
"data": {
|
||||
"percentage": 0.45,
|
||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
||||
"chapter": 3
|
||||
}
|
||||
},
|
||||
"kobo": {
|
||||
"source": "kobo",
|
||||
"timestamp": "2026-01-30T20:05:00Z",
|
||||
"data": {
|
||||
"percentage": 0.42,
|
||||
"location": "unknown"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resolution_status": "unresolved",
|
||||
"created_at": "2026-01-30T20:10:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Use this to get full details before resolving, showing exactly what data differs between sources.
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
meta {
|
||||
name: List Conflicts
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/conflicts?status=unresolved
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{token}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## List Conflicts
|
||||
|
||||
Lists all sync conflicts for the authenticated user with optional filtering.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/conflicts
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Query Parameters:**
|
||||
- `status` (string, optional): Filter by resolution status
|
||||
- `unresolved`: Only unresolved conflicts (default)
|
||||
- `user_resolved`: Conflicts resolved by user
|
||||
- `auto_resolved`: Automatically resolved conflicts
|
||||
- `all`: All conflicts regardless of status
|
||||
- `type` (string, optional): Filter by conflict type
|
||||
- `progress`: Reading progress conflicts
|
||||
- `note`: Bookmark/note conflicts
|
||||
- `highlight`: Highlight conflicts
|
||||
|
||||
**Response:**
|
||||
- `conflicts` (array): Array of conflict objects
|
||||
- `total` (number): Total number of conflicts matching filters
|
||||
- `unresolved` (number): Number of unresolved conflicts
|
||||
|
||||
**Each Conflict Object:**
|
||||
- `id` (string): Conflict UUID
|
||||
- `media_item_id` (string): Associated book UUID
|
||||
- `media_item_title` (string): Book title
|
||||
- `conflict_type` (string): Type of conflict (progress, note, highlight)
|
||||
- `conflict_data` (object): Side-by-side comparison of conflicting data
|
||||
- `koreader`: Data from KOReader device
|
||||
- `kobo`: Data from Kobo device
|
||||
- `web`: Data from web interface
|
||||
- `resolution_status` (string): Current status (unresolved, user_resolved, auto_resolved)
|
||||
- `created_at` (string): ISO 8601 timestamp when conflict was detected
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
|
||||
**Example Request:**
|
||||
```
|
||||
GET /api/conflicts?status=unresolved&type=progress
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"conflicts": [
|
||||
{
|
||||
"id": "conflict-uuid",
|
||||
"media_item_id": "book-uuid",
|
||||
"media_item_title": "Foundation",
|
||||
"conflict_type": "progress",
|
||||
"conflict_data": {
|
||||
"koreader": { "percentage": 0.65, "epubcfi": "..." },
|
||||
"kobo": { "percentage": 0.43, "epubcfi": "..." }
|
||||
},
|
||||
"resolution_status": "unresolved",
|
||||
"created_at": "2026-01-31T12:00:00Z"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"unresolved": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Conflicts occur when multiple devices update the same book data without syncing first.
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
meta {
|
||||
name: Resolve Conflict
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/conflicts/{{conflict_id}}/resolve
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"winner": "koreader",
|
||||
"manual_data": null,
|
||||
"apply_to_all_future_conflicts": false,
|
||||
"reason": "User chose more recent progress"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Resolve Conflict
|
||||
|
||||
Resolves a sync conflict by choosing which source to use for the conflicting data.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/conflicts/{conflict_id}/resolve
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Path Parameters:**
|
||||
- `conflict_id` (string): Conflict UUID
|
||||
|
||||
**Request Body:**
|
||||
- `winner` (string): Source to choose
|
||||
- `koreader`: Use KOReader device data
|
||||
- `kobo`: Use Kobo device data
|
||||
- `web`: Use web interface data
|
||||
- `manual`: Use custom merged data (requires manual_data)
|
||||
- `manual_data` (object, optional): Required if winner is "manual"
|
||||
- `percentage` (number): Reading progress percentage (0-1)
|
||||
- `epubcfi` (string): EPUB Canonical Fragment Identifier
|
||||
- `chapter` (number, optional): Chapter number
|
||||
- `page` (number, optional): Page number
|
||||
- `apply_to_all_future_conflicts` (boolean): Auto-resolve future conflicts from this source
|
||||
- `reason` (string, optional): Explanation for the resolution choice
|
||||
|
||||
**Response:**
|
||||
- `conflict_resolved` (boolean): True if successful
|
||||
- `applied_to` (string): What was updated (progress, annotations, etc.)
|
||||
- `devices_synced` (array): List of device IDs that were notified
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success - conflict resolved
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 404: Conflict not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Example - Choose KOReader:**
|
||||
```json
|
||||
{
|
||||
"winner": "koreader",
|
||||
"manual_data": null,
|
||||
"apply_to_all_future_conflicts": false,
|
||||
"reason": "More recent progress"
|
||||
}
|
||||
```
|
||||
|
||||
**Example - Manual Override:**
|
||||
```json
|
||||
{
|
||||
"winner": "manual",
|
||||
"manual_data": {
|
||||
"percentage": 0.43,
|
||||
"epubcfi": "epubcfi(/6/4/2:20)",
|
||||
"chapter": 3
|
||||
},
|
||||
"apply_to_all_future_conflicts": false,
|
||||
"reason": "Custom merged position"
|
||||
}
|
||||
```
|
||||
|
||||
**After Resolution:**
|
||||
- Winning data is applied to reading progress
|
||||
- All connected devices notified via WebSocket
|
||||
- Conflict status changes to "user_resolved"
|
||||
- Resolution stored for audit trail
|
||||
|
||||
**Note:** Manual override allows precise control when automatic resolution doesn't capture the correct state.
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
meta {
|
||||
name: Add Books to Kobo Shelf
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseURL}}/api/devices/{{deviceID}}/shelves
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{userToken}}
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"media_item_ids": [
|
||||
"{{bookUUID1}}",
|
||||
"{{bookUUID2}}"
|
||||
],
|
||||
"shelf_name": "Reading List",
|
||||
"shelf_position": 0
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Add Books to Kobo Shelf
|
||||
|
||||
Add one or more books to a Kobo device shelf. Manages which books should be synced to a specific Kobo device. Supports multiple shelves for organization.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/devices/{deviceID}/shelves
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Path Parameters:**
|
||||
- `deviceID` (string): Device UUID
|
||||
|
||||
**Request Body:**
|
||||
- `media_item_ids` (array): Array of book UUIDs to add
|
||||
- `shelf_name` (string): Name of the shelf (e.g., "Reading List", "Favorites")
|
||||
- `shelf_position` (number, optional): Position on the shelf (default: 0)
|
||||
|
||||
**Response:**
|
||||
- `message` (string): Success message
|
||||
- `added_count` (number): Number of books added
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success - books added to shelf
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 404: Device or one or more books not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Note:** Adding a book that's already on the shelf updates its position if a new position is specified. Supports multiple shelves for organizing content on the Kobo device.
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
meta {
|
||||
name: Approve Device Registration
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/devices/approve/{{registration_id}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
docs {
|
||||
## Approve Device Registration
|
||||
|
||||
Approves a pending device registration request.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/devices/approve/:registration_id
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Path Parameters:**
|
||||
- `registration_id` (string): Registration request UUID
|
||||
|
||||
**Response:**
|
||||
- Success message with approved device details
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 404: Registration not found
|
||||
- 400: Invalid registration status
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"message": "Device registration approved",
|
||||
"device_id": "uuid",
|
||||
"device_name": "My Kobo"
|
||||
}
|
||||
```
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
meta {
|
||||
name: Check Registration Status
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/devices/register/status
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"registration_id": "{{registrationId}}"
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
meta {
|
||||
name: Clear Kobo Shelf
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{baseURL}}/api/devices/{{deviceID}}/shelves/clear?shelf={{shelfName}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{userToken}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Clear Kobo Shelf
|
||||
|
||||
Clear all books from a Kobo device shelf, or all shelves if no shelf name is specified.
|
||||
|
||||
**Method:** DELETE
|
||||
|
||||
**Endpoint:** /api/devices/{deviceID}//shelves/clear?shelf={shelfName}
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Path Parameters:**
|
||||
- `deviceID` (string): Device UUID
|
||||
|
||||
**Query Parameters:**
|
||||
- `shelf` (string, optional): Shelf name to clear. If omitted, clears all shelves.
|
||||
|
||||
**Response:**
|
||||
- `message` (string): Success message
|
||||
- `cleared_count` (number): Number of books removed from shelf
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success - shelf cleared
|
||||
- 401: Unauthorized
|
||||
- 404: Device not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Note:** Removes all books from the specified shelf. If no shelf name is provided, clears all shelves for the device. This operation cannot be undone.
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
meta {
|
||||
name: Create Device File Alias
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/devices/{{device_id}}/file-aliases
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"file_path": "/mnt/sd/books/my-book.kepub.epub",
|
||||
"media_item_id": "{{media_item_id}}"
|
||||
}
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_create_device_file_alias_success(status, headers, body) {
|
||||
if (status !== 201 && status !== 200) {
|
||||
throw new Error("Expected status 201 or 200, got " + status);
|
||||
}
|
||||
|
||||
const contentType = headers["content-type"];
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
throw new Error("Expected content-type to contain application/json");
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(body);
|
||||
} catch (e) {
|
||||
throw new Error("Response body is not valid JSON");
|
||||
}
|
||||
|
||||
if (!data.id) {
|
||||
throw new Error("Response missing id field");
|
||||
}
|
||||
|
||||
if (!data.file_path) {
|
||||
throw new Error("Response missing file_path field");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
deviceId: "8821b703-1234-5678-9123-446655440002"
|
||||
mediaItemId: "8821b703-1234-5678-9123-446655440001"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Create Device File Alias
|
||||
|
||||
Creates a new file alias for a device. File aliases map device-specific file paths to media items.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/devices/:id/file-aliases
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string, required): Device UUID
|
||||
|
||||
**Request Body:**
|
||||
- `file_path` (string, required): Device-specific file path
|
||||
- `media_item_id` (string, required): Media item UUID to link to
|
||||
|
||||
**Response:** Created file alias object
|
||||
- `id` (string): Alias UUID
|
||||
- `device_id` (string): Device UUID
|
||||
- `file_path` (string): Device-specific file path
|
||||
- `media_item_id` (string): Associated media item UUID
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 201: Created
|
||||
- 200: Success
|
||||
- 400: Invalid request body
|
||||
- 401: Unauthorized
|
||||
- 404: Device not found
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
meta {
|
||||
name: Delete Device
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{base_url}}/api/devices/{{deviceId}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{token}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Delete Device
|
||||
|
||||
Deletes a device and unregisters it from the user's account.
|
||||
|
||||
**Method:** DELETE
|
||||
|
||||
**Endpoint:** /api/devices/{deviceId}
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Path Parameters:**
|
||||
- `deviceId` (string): Device UUID
|
||||
|
||||
**Response:** 204 No Content on success
|
||||
|
||||
**Status Codes:**
|
||||
- 204: Success - device deleted
|
||||
- 401: Unauthorized
|
||||
- 404: Device not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Note:** This action cannot be undone. All device data and sync history will be removed.
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
meta {
|
||||
name: Get Device File Aliases
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/devices/{{device_id}}/file-aliases
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_get_device_file_aliases_success(status, headers, body) {
|
||||
if (status !== 200) {
|
||||
throw new Error("Expected status 200, got " + status);
|
||||
}
|
||||
|
||||
const contentType = headers["content-type"];
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
throw new Error("Expected content-type to contain application/json");
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(body);
|
||||
} catch (e) {
|
||||
throw new Error("Response body is not valid JSON");
|
||||
}
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error("Expected response body to be an array");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
deviceId: "8821b703-1234-5678-9123-446655440002"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Device File Aliases
|
||||
|
||||
Retrieves all file aliases for a specific device. File aliases are used to map device-specific file paths to media items.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/devices/:id/file-aliases
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string, required): Device UUID
|
||||
|
||||
**Response:** Array of file alias objects
|
||||
- `id` (string): Alias UUID
|
||||
- `device_id` (string): Device UUID
|
||||
- `file_path` (string): Device-specific file path
|
||||
- `media_item_id` (string): Associated media item UUID
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 404: Device not found
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
meta {
|
||||
name: Get Device
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/devices/{{deviceId}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{token}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Device
|
||||
|
||||
Retrieves detailed information about a specific device.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/devices/{deviceId}
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Path Parameters:**
|
||||
- `deviceId` (string): Device UUID
|
||||
|
||||
**Response:**
|
||||
- `id` (string): Device UUID
|
||||
- `name` (string): Device name
|
||||
- `device_type` (string): Type (kobo, koreader, web)
|
||||
- `last_sync` (string): Last sync timestamp
|
||||
- `is_active` (boolean): Whether device is active
|
||||
- `created_at` (string): Registration timestamp
|
||||
- `sync_settings` (object): Device-specific sync settings
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 404: Device not found
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
meta {
|
||||
name: Get Kobo Shelf Books
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{baseURL}}/api/devices/{{deviceID}}/shelves?shelf={{shelfName}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{userToken}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Kobo Shelf Books
|
||||
|
||||
Get all books on a Kobo device shelf, optionally filter by shelf name.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/devices/{deviceID}/shelves?shelf={shelfName}
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Path Parameters:**
|
||||
- `deviceID` (string): Device UUID
|
||||
|
||||
**Query Parameters:**
|
||||
- `shelf` (string, optional): Shelf name to filter by. If omitted, returns all shelves.
|
||||
|
||||
**Response:**
|
||||
- Array of books with:
|
||||
- `id` (string): Book UUID
|
||||
- `title` (string): Book title
|
||||
- `author` (string): Book author
|
||||
- `shelf_name` (string): Name of the shelf
|
||||
- `shelf_position` (number): Position on the shelf
|
||||
- `entitlement_id` (string): Kobo entitlement ID
|
||||
- `revision_id` (string): Kobo revision number
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 404: Device not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Note:** Returns Kobo-specific metadata (entitlement ID, revision number) required for proper Kobo sync operations.
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
meta {
|
||||
name: Initiate Device Registration
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/devices/register
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"device_name": "My Kindle Paperwhite",
|
||||
"device_type": "koreader",
|
||||
"device_identifier": "kindle-pw5-hardware-id-12345"
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
meta {
|
||||
name: List Devices
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/devices
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{token}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## List Devices
|
||||
|
||||
Lists all devices registered to the authenticated user's account.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/devices
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Response:**
|
||||
- `devices` (array): Array of device objects
|
||||
- `id` (string): Device UUID
|
||||
- `name` (string): Device name
|
||||
- `device_type` (string): Type (kobo, koreader, web)
|
||||
- `last_sync` (string): Last sync timestamp
|
||||
- `is_active` (boolean): Whether device is active
|
||||
- `created_at` (string): Registration timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
meta {
|
||||
name: List Pending Device Registrations
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/devices/pending
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
docs {
|
||||
## List Pending Device Registrations
|
||||
|
||||
Retrieves all pending device registration requests awaiting approval.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/devices/pending
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Response:**
|
||||
- Array of pending device registrations
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"device_name": "My Kobo",
|
||||
"device_type": "kobo",
|
||||
"user_id": "uuid",
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
meta {
|
||||
name: Reject Device Registration
|
||||
type: http
|
||||
seq: 7
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/devices/reject/{{registration_id}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
docs {
|
||||
## Reject Device Registration
|
||||
|
||||
Rejects a pending device registration request.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/devices/reject/:registration_id
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Path Parameters:**
|
||||
- `registration_id` (string): Registration request UUID
|
||||
|
||||
**Response:**
|
||||
- Success message confirming rejection
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 404: Registration not found
|
||||
- 400: Invalid registration status
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"message": "device registration rejected"
|
||||
}
|
||||
```
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
meta {
|
||||
name: Remove Book from Kobo Shelf
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{baseURL}}/api/devices/{{deviceID}}/shelves?media_item_id={{bookUUID}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{userToken}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Remove Book from Kobo Shelf
|
||||
|
||||
Remove a specific book from a Kobo device shelf, preventing it from syncing to that device.
|
||||
|
||||
**Method:** DELETE
|
||||
|
||||
**Endpoint:** /api/devices/{deviceID}/shelves?media_item_id={bookUUID}
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Path Parameters:**
|
||||
- `deviceID` (string): Device UUID
|
||||
|
||||
**Query Parameters:**
|
||||
- `media_item_id` (string): Book UUID to remove from shelf
|
||||
|
||||
**Response:**
|
||||
- `message` (string): Success message
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success - book removed from shelf
|
||||
- 401: Unauthorized
|
||||
- 404: Device or book not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Note:** Removing a book from the device shelf prevents it from syncing to that device in future sync operations.
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
meta {
|
||||
name: Update Device
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{base_url}}/api/devices/{{deviceId}}
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"device_name": "My Updated Kindle",
|
||||
"sync_enabled": true,
|
||||
"auto_sync": true,
|
||||
"sync_frequency_minutes": 10
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
meta {
|
||||
name: "Bookhoard Device Management API"
|
||||
type: "collection"
|
||||
environment: {
|
||||
development: {
|
||||
base_url: "http://localhost:8765/api"
|
||||
},
|
||||
production: {
|
||||
base_url: "https://your-domain.com/api"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Register Device
|
||||
@name("Register Device")
|
||||
POST {{environment.base_url}}/devices/register
|
||||
Content-Type: application/json
|
||||
{
|
||||
"device_name": "My Kobo Clara",
|
||||
"device_type": "kobo",
|
||||
"device_identifier": "N1234567890123"
|
||||
}
|
||||
|
||||
@name("Register KOReader Device")
|
||||
POST {{environment.base_url}}/devices/register
|
||||
Content-Type: application/json
|
||||
{
|
||||
"device_name": "My Kindle Paperwhite",
|
||||
"device_type": "koreader",
|
||||
"device_identifier": "G090GP123456789"
|
||||
}
|
||||
|
||||
@name("Register Web Device")
|
||||
POST {{environment.base_url}}/devices/register
|
||||
Content-Type: application/json
|
||||
{
|
||||
"device_name": "Chrome Browser",
|
||||
"device_type": "web",
|
||||
"device_identifier": "web-client-abc123"
|
||||
}
|
||||
|
||||
# Check Registration Status
|
||||
@name("Check Pending Registration")
|
||||
POST {{environment.base_url}}/devices/register/status
|
||||
Content-Type: application/json
|
||||
{
|
||||
"registration_id": "registration-uuid-here"
|
||||
}
|
||||
|
||||
@name("Check Approved Registration")
|
||||
POST {{environment.base_url}}/devices/register/status
|
||||
Content-Type: application/json
|
||||
{
|
||||
"registration_id": "registration-uuid-here"
|
||||
}
|
||||
|
||||
# List Devices (requires authentication)
|
||||
@name("List User Devices")
|
||||
GET {{environment.base_url}}/devices
|
||||
Authorization: Bearer {{jwt_token}}
|
||||
|
||||
@name("Get Device Details")
|
||||
GET {{environment.base_url}}/devices/{{device_id}}
|
||||
Authorization: Bearer {{jwt_token}}
|
||||
|
||||
# Update Device Settings
|
||||
@name("Update Device Settings")
|
||||
PUT {{environment.base_url}}/devices/{{device_id}}
|
||||
Authorization: Bearer {{jwt_token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"device_name": "Updated Device Name",
|
||||
"sync_enabled": true,
|
||||
"auto_sync": true,
|
||||
"sync_frequency_minutes": 10
|
||||
}
|
||||
|
||||
@name("Disable Device Sync")
|
||||
PUT {{environment.base_url}}/devices/{{device_id}}
|
||||
Authorization: Bearer {{jwt_token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"device_name": "My Kobo Clara",
|
||||
"sync_enabled": false,
|
||||
"auto_sync": false,
|
||||
"sync_frequency_minutes": 30
|
||||
}
|
||||
|
||||
@name("Update Sync Frequency")
|
||||
PUT {{environment.base_url}}/devices/{{device_id}}
|
||||
Authorization: Bearer {{jwt_token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"device_name": "My Kobo Clara",
|
||||
"sync_enabled": true,
|
||||
"auto_sync": true,
|
||||
"sync_frequency_minutes": 15
|
||||
}
|
||||
|
||||
# Delete/Revoke Device
|
||||
@name("Delete Device")
|
||||
DELETE {{environment.base_url}}/devices/{{device_id}}
|
||||
Authorization: Bearer {{jwt_token}}
|
||||
|
||||
# Get Pending Registrations
|
||||
@name("Get Pending Registrations")
|
||||
GET {{environment.base_url}}/devices/pending
|
||||
Authorization: Bearer {{jwt_token}}
|
||||
|
||||
# Approve Device Registration
|
||||
@name("Approve Device Registration")
|
||||
GET {{environment.base_url}}/devices/approve/{{registration_id}}
|
||||
Authorization: Bearer {{jwt_token}}
|
||||
|
||||
@name("Approve Registration - KOReader")
|
||||
GET {{environment.base_url}}/devices/approve/reg-uuid-123
|
||||
Authorization: Bearer {{jwt_token}}
|
||||
|
||||
@name("Approve Registration - Kobo")
|
||||
GET {{environment.base_url}}/devices/approve/reg-uuid-456
|
||||
Authorization: Bearer {{jwt_token}}
|
||||
|
||||
# Reject Device Registration
|
||||
@name("Reject Device Registration")
|
||||
POST {{environment.base_url}}/devices/reject/{{registration_id}}
|
||||
Authorization: Bearer {{jwt_token}}
|
||||
|
||||
@name("Reject Registration - KOReader")
|
||||
POST {{environment.base_url}}/devices/reject/reg-uuid-123
|
||||
Authorization: Bearer {{jwt_token}}
|
||||
|
||||
@name("Reject Registration - Kobo")
|
||||
POST {{environment.base_url}}/devices/reject/reg-uuid-456
|
||||
Authorization: Bearer {{jwt_token}}
|
||||
@@ -1,26 +0,0 @@
|
||||
meta {
|
||||
name: Regenerate Device Token - Forbidden
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{base_url}}/api/devices/{{other_device_id}}/regenerate-token
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
docs {
|
||||
## Regenerate Device Token - Forbidden
|
||||
|
||||
Tests that users cannot regenerate tokens for devices belonging to other users.
|
||||
|
||||
**Expected Behavior:** Returns 403 Forbidden when trying to regenerate token for another user's device
|
||||
|
||||
**Status Codes:**
|
||||
- 403: Forbidden (device belongs to different user)
|
||||
|
||||
**Use Case:** Verify authorization - users can only manage their own devices
|
||||
|
||||
**Setup:** Use Bearer token from user A, try to regenerate token for user B's device
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
meta {
|
||||
name: Regenerate Device Token - Not Found
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{base_url}}/api/devices/00000000-0000-0000-0000-000000000000/regenerate-token
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
docs {
|
||||
## Regenerate Device Token - Not Found
|
||||
|
||||
Tests that token regeneration returns 404 for non-existent devices.
|
||||
|
||||
**Expected Behavior:** Returns 404 Not Found when device UUID doesn't exist
|
||||
|
||||
**Status Codes:**
|
||||
- 404: Device not found
|
||||
|
||||
**Use Case:** Verify proper error handling for invalid device IDs
|
||||
|
||||
**Setup:** Use all-zero UUID (guaranteed to not exist in database)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
meta {
|
||||
name: Regenerate Device Token - Unauthorized
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{base_url}}/api/devices/{{device_id}}/regenerate-token
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
docs {
|
||||
## Regenerate Device Token - Unauthorized
|
||||
|
||||
Tests that token regeneration requires authentication.
|
||||
|
||||
**Expected Behavior:** Returns 401 Unauthorized when no Bearer token is provided
|
||||
|
||||
**Status Codes:**
|
||||
- 401: Unauthorized (missing or invalid token)
|
||||
|
||||
**Use Case:** Verify authentication is required for token regeneration
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
meta {
|
||||
name: Regenerate Device Token
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{base_url}}/api/devices/{{device_id}}/regenerate-token
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
docs {
|
||||
## Regenerate Device Token
|
||||
|
||||
Regenerates auth token for a device, invalidating old token immediately.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/devices/{device_id}/regenerate-token
|
||||
|
||||
**Authentication:** Bearer token (JWT)
|
||||
|
||||
**Path Parameters:**
|
||||
- `device_id` (string): Device UUID
|
||||
|
||||
**Response:**
|
||||
- `message` (string): Success message
|
||||
- `auth_token` (string): New auth token
|
||||
- `device` (object): Updated device details
|
||||
- `sync_urls` (object): Device-specific sync URLs with new token
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (device belongs to different user)
|
||||
- 404: Device not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Important Notes:**
|
||||
- Old token stops working immediately
|
||||
- Device must be updated with new token to resume syncing
|
||||
- No data loss - device ID remains same
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"message": "Token regenerated successfully",
|
||||
"auth_token": "dev_abc123...",
|
||||
"device": {
|
||||
"id": "uuid-here",
|
||||
"device_name": "My Kobo Clara",
|
||||
"device_type": "kobo",
|
||||
"sync_enabled": true,
|
||||
"auto_sync": true,
|
||||
"sync_frequency_minutes": 5,
|
||||
"created_at": "2026-02-12T10:00:00Z",
|
||||
"device_metadata": "{...}"
|
||||
},
|
||||
"sync_urls": {
|
||||
"sync_url": "http://localhost:8765/api/sync/kobo/dev_new_token",
|
||||
"markup": "http://localhost:8765/api/sync/kobo/dev_new_token/markup",
|
||||
"bookmark": "http://localhost:8765/api/sync/kobo/dev_new_token/bookmark",
|
||||
"init": "http://localhost:8765/api/sync/kobo/dev_new_token/v1/initialization"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
- Old token stops working immediately
|
||||
- Device must be updated with new token to resume syncing
|
||||
- No data loss - device ID remains same
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
vars {
|
||||
base_url: http://localhost:8765
|
||||
media_item_id: 02a535a4-19f8-43fa-b81b-89a226d19dd9
|
||||
fake_book_id: 123e4567-e89b-12d3-a456-426614174000
|
||||
user_id: c51118f0-31fc-4c32-827d-517d6599bf21
|
||||
highlight_id: 660f9501-f29b-51d4-b716-446655440001
|
||||
note_id: 7710a602-g29b-61d4-c716-446655440002
|
||||
library_id: cc23c3a7-f8fb-451a-a78d-2a16df1b725a
|
||||
job_id: 550e8400-e29b-41d4-a716-446655440000
|
||||
rating: 5
|
||||
is_visible: true
|
||||
library_folder: /app/uploads
|
||||
opds_base_url:
|
||||
}
|
||||
vars:secret [
|
||||
token,
|
||||
refresh_token,
|
||||
kobo_device_token,
|
||||
other_device_id
|
||||
]
|
||||
@@ -1,59 +0,0 @@
|
||||
meta {
|
||||
name: Create Media Highlight
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/media-items/{{media_item_id}}/highlights
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"selection_text": "This is the highlighted text from the media item.",
|
||||
"start_position": "page:45:offset:120",
|
||||
"end_position": "page:45:offset:145",
|
||||
"color": "#ffff00",
|
||||
"note_id": ""
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Create Media Highlight
|
||||
|
||||
Creates a new highlight for a specific media item.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/media-items/:id/highlights
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Media item ID
|
||||
|
||||
**Request Body:**
|
||||
- `selection_text` (string): Highlighted text (required, 1-5000 chars)
|
||||
- `start_position` (string): Start position (required, max 100 chars)
|
||||
- `end_position` (string): End position (required, max 100 chars)
|
||||
- `color` (string): Highlight color in hex format (optional, default #ffff00)
|
||||
- `note_id` (string): Optional associated note ID
|
||||
|
||||
**Response:**
|
||||
- Highlight object with all fields including generated ID and timestamps
|
||||
|
||||
**Status Codes:**
|
||||
- 201: Created
|
||||
- 400: Invalid request
|
||||
- 401: Unauthorized
|
||||
- 404: Media item not found
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
meta {
|
||||
name: Delete Media Highlight
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{base_url}}/api/media-items/{{media_item_id}}/highlights/{{highlight_id}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Delete Media Highlight
|
||||
|
||||
Deletes a specific highlight.
|
||||
|
||||
**Method:** DELETE
|
||||
|
||||
**Endpoint:** /api/media-items/:id/highlights/:highlightId
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Media item ID
|
||||
- `highlightId` (string): Highlight ID
|
||||
|
||||
**Response:**
|
||||
- 204 No Content on success
|
||||
|
||||
**Status Codes:**
|
||||
- 204: Success
|
||||
- 401: Unauthorized
|
||||
- 404: Highlight not found
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
meta {
|
||||
name: Get Media Highlights
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/media-items/{{media_item_id}}/highlights
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Media Highlights
|
||||
|
||||
Retrieves all highlights for a specific media item for the authenticated user.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/media-items/:id/highlights
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Media item ID
|
||||
|
||||
**Response:**
|
||||
- Array of highlight objects with fields:
|
||||
- `id` (string): Highlight ID
|
||||
- `media_item_id` (string): Media item ID
|
||||
- `user_id` (string): User ID
|
||||
- `selection_text` (string): Highlighted text
|
||||
- `start_position` (string): Start position
|
||||
- `end_position` (string): End position
|
||||
- `color` (string): Highlight color (hex)
|
||||
- `note_id` (string): Optional associated note ID
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 404: Media item not found
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
meta {
|
||||
name: Get Single Media Highlight
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/media-items/{{media_item_id}}/highlights/{{highlight_id}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Single Media Highlight
|
||||
|
||||
Retrieves a specific highlight by ID.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/media-items/:id/highlights/:highlightId
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Media item ID
|
||||
- `highlightId` (string): Highlight ID
|
||||
|
||||
**Response:**
|
||||
- Highlight object with all fields
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 404: Highlight not found
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
meta {
|
||||
name: Update Media Highlight
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{base_url}}/api/media-items/{{media_item_id}}/highlights/{{highlight_id}}
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"selection_text": "This is the updated highlighted text.",
|
||||
"start_position": "page:45:offset:125",
|
||||
"end_position": "page:45:offset:150",
|
||||
"color": "#ffeb3b",
|
||||
"note_id": ""
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Update Media Highlight
|
||||
|
||||
Updates an existing highlight.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/media-items/:id/highlights/:highlightId
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Media item ID
|
||||
- `highlightId` (string): Highlight ID
|
||||
|
||||
**Request Body:**
|
||||
- `selection_text` (string): Updated highlighted text (required, 1-5000 chars)
|
||||
- `start_position` (string): Updated start position (required, max 100 chars)
|
||||
- `end_position` (string): Updated end position (required, max 100 chars)
|
||||
- `color` (string): Updated highlight color in hex format (optional)
|
||||
- `note_id` (string): Updated associated note ID (optional)
|
||||
|
||||
**Response:**
|
||||
- Updated highlight object with all fields
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid request
|
||||
- 401: Unauthorized
|
||||
- 404: Highlight not found
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
meta {
|
||||
name: Sync from Bookhoard to Kobo
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseURL}}/api/sync/kobo/sync-from-server
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{koboToken}}
|
||||
Content-Type: application/json
|
||||
x-kobo-device: {"DeviceId":"kobo-clara-test","Model":"Kobo Clara","SerialNumber":"N123456789"}
|
||||
}
|
||||
|
||||
body:json {
|
||||
[
|
||||
{
|
||||
"ContentId": "{{bookUUID}}",
|
||||
"PercentRead": 65.4,
|
||||
"LastModified": "2026-01-31T12:00:00Z",
|
||||
"Bookmarks": [
|
||||
{
|
||||
"BookmarkId": "bookmark-123",
|
||||
"ContentId": "{{bookUUID}}",
|
||||
"BookmarkText": "This is an important note",
|
||||
"BookmarkType": "bookmark",
|
||||
"BookmarkTitle": "Chapter 5 Note"
|
||||
}
|
||||
],
|
||||
"Highlights": [
|
||||
{
|
||||
"BookmarkId": "highlight-456",
|
||||
"ContentId": "{{bookUUID}}",
|
||||
"BookmarkText": "highlighted passage text",
|
||||
"BookmarkType": "annotation",
|
||||
"BookmarkTitle": "Chapter 3 Highlight"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Sync from Bookhoard to Kobo
|
||||
|
||||
Server-initiated sync pushing progress, bookmarks, and highlights from Bookhoard to Kobo device. Two-way sync endpoint.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/sync/kobo/sync-from-server
|
||||
|
||||
**Authentication:** Bearer token with Kobo device identification
|
||||
|
||||
**Headers:**
|
||||
- `x-kobo-device` (string): JSON string containing Kobo device info
|
||||
- `DeviceId`: Kobo device ID
|
||||
- `Model`: Kobo device model
|
||||
- `SerialNumber`: Kobo device serial number
|
||||
|
||||
**Request Body:** Array of sync data objects
|
||||
- `ContentId` (string): Book UUID
|
||||
- `PercentRead` (number): Reading progress percentage (0-100)
|
||||
- `LastModified` (string): ISO 8601 timestamp
|
||||
- `Bookmarks` (array, optional): Array of bookmark objects
|
||||
- `BookmarkId`: Unique bookmark ID
|
||||
- `ContentId`: Book UUID
|
||||
- `BookmarkText`: Bookmark text/note
|
||||
- `BookmarkType`: Type (bookmark, annotation, etc.)
|
||||
- `BookmarkTitle`: Bookmark title
|
||||
- `Highlights` (array, optional): Array of highlight objects (same structure as bookmarks)
|
||||
|
||||
**Response:**
|
||||
- Sync result confirmation
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
|
||||
**Note:** Allows Bookhoard server to push updates to Kobo device, including reading progress, bookmarks, and highlights.
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
meta {
|
||||
name: Kobo Bookmark Sync
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/v1/kobo/bookmark
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{device_token}}
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"BookmarkSync": [
|
||||
{
|
||||
"BookmarkId": "bookmark_2",
|
||||
"ContentId": "kobo_xyz789",
|
||||
"BookmarkText": "Important note",
|
||||
"BookmarkType": "bookmark",
|
||||
"DateCreated": "2026-01-31T12:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
const body = res.getBody();
|
||||
tests('Status is Success', body.Status === "Success");
|
||||
tests('BookmarksSynced >= 0', body.BookmarksSynced >= 0);
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Kobo Bookmark Sync
|
||||
|
||||
Synchronizes bookmarks from a Kobo device to the Bookhoard server.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/v1/kobo/bookmark
|
||||
|
||||
**Authentication:** Bearer token (device token)
|
||||
|
||||
**Request Body:**
|
||||
- `BookmarkSync` (array): Array of bookmark objects
|
||||
- `BookmarkId` (string): Unique bookmark ID
|
||||
- `ContentId` (string): Book/content ID
|
||||
- `BookmarkText` (string): Bookmark text or note
|
||||
- `BookmarkType` (string): Type (bookmark, highlight, note)
|
||||
- `DateCreated` (string): ISO 8601 timestamp
|
||||
|
||||
**Response:**
|
||||
- `Status` (string): Sync status (Success, Partial)
|
||||
- `BookmarksSynced` (number): Number of bookmarks synced
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
meta {
|
||||
name: Kobo Initialization
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/v1/kobo/initialization
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{device_token}}
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
const body = res.getBody();
|
||||
tests('Has ContentId', body.ContentId !== undefined);
|
||||
tests('Has Categories', body.Categories !== undefined);
|
||||
tests('Has BookhoardUUID', body.BookhoardUUID !== undefined);
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Kobo Initialization
|
||||
|
||||
Initializes Kobo device sync, returning device resources and account information.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/v1/kobo/initialization
|
||||
|
||||
**Authentication:** Bearer token (device token)
|
||||
|
||||
**Response:**
|
||||
- `ContentId` (string): Device content ID
|
||||
- `Categories` (array): Available categories/collections
|
||||
- `BookhoardUUID` (string): Bookhoard instance UUID
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
meta {
|
||||
name: Kobo Markup Sync
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/v1/kobo/markup
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{device_token}}
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"ReadingSync": [
|
||||
{
|
||||
"ContentId": "kobo_abc123def456",
|
||||
"PercentRead": 60.0,
|
||||
"RemainingTimeMin": 120,
|
||||
"ReadingEvent": "BookRead",
|
||||
"LastModified": "2026-01-31T12:00:00Z"
|
||||
}
|
||||
],
|
||||
"BookmarkSync": [
|
||||
{
|
||||
"BookmarkId": "bookmark_1",
|
||||
"ContentId": "kobo_abc123def456",
|
||||
"BookmarkText": "Great quote",
|
||||
"BookmarkType": "annotation",
|
||||
"DateCreated": "2026-01-31T12:00:00Z"
|
||||
}
|
||||
],
|
||||
"Metadata": true
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
const body = res.getBody();
|
||||
const validStatus = body.Status === "Success" || body.Status === "Partial";
|
||||
tests('Status is Success or Partial', validStatus);
|
||||
tests('MarkupsSynced >= 0', body.MarkupsSynced >= 0);
|
||||
tests('BookmarksSynced >= 0', body.BookmarksSynced >= 0);
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Kobo Markup Sync
|
||||
|
||||
Synchronizes reading progress and markup (highlights, bookmarks) from a Kobo device.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/v1/kobo/markup
|
||||
|
||||
**Authentication:** Bearer token (device token)
|
||||
|
||||
**Request Body:**
|
||||
- `ReadingSync` (array, optional): Reading progress data
|
||||
- `ContentId` (string): Book/content ID
|
||||
- `PercentRead` (number): Percentage read (0-100)
|
||||
- `RemainingTimeMin` (number): Remaining time in minutes
|
||||
- `ReadingEvent` (string): Event type (BookRead, etc.)
|
||||
- `LastModified` (string): ISO 8601 timestamp
|
||||
- `BookmarkSync` (array, optional): Bookmark/highlight data
|
||||
- `BookmarkId` (string): Unique bookmark ID
|
||||
- `ContentId` (string): Book/content ID
|
||||
- `BookmarkText` (string): Highlighted/bookmarked text
|
||||
- `BookmarkType` (string): Type (annotation, bookmark)
|
||||
- `DateCreated` (string): ISO 8601 timestamp
|
||||
- `Metadata` (boolean): Whether to include metadata
|
||||
|
||||
**Response:**
|
||||
- `Status` (string): Sync status (Success, Partial)
|
||||
- `MarkupsSynced` (number): Number of markups synced
|
||||
- `BookmarksSynced` (number): Number of bookmarks synced
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
meta {
|
||||
name: KOReader Get Book Metadata
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/sync/koreader/metadata/{{book_uuid}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{device_token}}
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
const body = res.getBody();
|
||||
tests['Status is 200'] = true;
|
||||
tests['Has UUID'] = body.uuid !== null;
|
||||
tests['Has title'] = body.title !== null;
|
||||
tests['Has progress'] = body.progress !== null;
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## KOReader Get Book Metadata
|
||||
|
||||
Retrieves metadata for a specific book from the KOReader sync endpoint.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/sync/koreader/metadata/{book_uuid}
|
||||
|
||||
**Authentication:** Bearer token (device token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `book_uuid` (string): Book UUID
|
||||
|
||||
**Response:**
|
||||
- `uuid` (string): Book UUID
|
||||
- `title` (string): Book title
|
||||
- `progress` (object): Reading progress data
|
||||
- `metadata` (object): Additional book metadata
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 404: Book not found
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
meta {
|
||||
name: KOReader Get Library
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/sync/koreader/library
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{device_token}}
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
const body = res.getBody();
|
||||
tests['Status is 200'] = res.getStatus() === 200;
|
||||
tests['Has library_sync'] = body.library_sync != null;
|
||||
tests('Total books >= 0', body.total_books >= 0);
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## KOReader Get Library
|
||||
|
||||
Retrieves the user's library for KOReader sync operations.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/sync/koreader/library
|
||||
|
||||
**Authentication:** Bearer token (device token)
|
||||
|
||||
**Response:**
|
||||
- `library_sync` (object): Library sync data
|
||||
- `total_books` (number): Total number of books
|
||||
- `books` (array): Array of book objects
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
meta {
|
||||
name: KOReader Sync Annotations - Per-Book SHA-256
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/v1/koreader/sync/bookmarks
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{koreader_device_token}}
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"book_uuid": "{{book_uuid}}",
|
||||
"highlights": [
|
||||
{
|
||||
"text": "Quote from book 1",
|
||||
"pos0": "/6/4[chap1ref]!/4/2/1:0",
|
||||
"pos1": "/6/4[chap1ref]!/4/2/1:50",
|
||||
"color": "#ffff00",
|
||||
"page": 10,
|
||||
"book_sha256": "{{book_sha256}}"
|
||||
},
|
||||
{
|
||||
"text": "Quote from book 2 (different book)",
|
||||
"pos0": "/6/4[chap1ref]!/4/2/1:0",
|
||||
"pos1": "/6/4[chap1ref]!/4/2/1:50",
|
||||
"color": "#00ff00",
|
||||
"page": 15,
|
||||
"book_sha256": "{{another_book_sha256}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
const body = res.getBody();
|
||||
tests('Highlights synced >= 0', body.highlights_synced >= 0);
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## KOReader Sync Annotations - Per-Book SHA-256
|
||||
|
||||
Synchronizes annotations (highlights) from KOReader with per-annotation SHA-256 hashes for multi-book sync.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/v1/koreader/sync/bookmarks
|
||||
|
||||
**Authentication:** Bearer token (KOReader device token)
|
||||
|
||||
**Request Body:**
|
||||
- `book_uuid` (string): Primary book UUID
|
||||
- `highlights` (array): Array of highlight objects
|
||||
- `text` (string): Highlighted text
|
||||
- `pos0`, `pos1` (string): EPUB CFI positions
|
||||
- `color` (string): Highlight color (hex)
|
||||
- `page` (number): Page number
|
||||
- `book_sha256` (string): SHA-256 hash for this specific book
|
||||
|
||||
**Response:**
|
||||
- `highlights_synced` (number): Number of highlights synced
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
|
||||
**Note:** Each highlight can include its own book_sha256, allowing annotations from multiple books in a single request.
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
meta {
|
||||
name: KOReader Sync Bookmarks - SHA-256
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/v1/koreader/sync/bookmarks
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{koreader_device_token}}
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"book_sha256": "{{book_sha256}}",
|
||||
"bookmarks": [
|
||||
{
|
||||
"text": "Important passage about chapter 3",
|
||||
"pos0": "/6/4[chap3ref]!/4/2/1:0",
|
||||
"page": 45,
|
||||
"type": "bookmark"
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
{
|
||||
"notes": "My note about this section",
|
||||
"pos0": "/6/4[chap3ref]!/4/2/1:100",
|
||||
"page": 47,
|
||||
"text": "Quoted text from the book"
|
||||
}
|
||||
],
|
||||
"highlights": [
|
||||
{
|
||||
"text": "This is highlighted text",
|
||||
"pos0": "/6/4[chap3ref]!/4/2/1:50",
|
||||
"pos1": "/6/4[chap3ref]!/4/2/1:100",
|
||||
"color": "#ffff00",
|
||||
"page": 50
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
const body = res.getBody();
|
||||
tests('Sync completed', body.sync_status === "completed");
|
||||
tests('Total synced >= 0', body.total_synced >= 0);
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## KOReader Sync Bookmarks - SHA-256
|
||||
|
||||
Synchronizes bookmarks, notes, and highlights from KOReader using SHA-256 hash for book identification.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/v1/koreader/sync/bookmarks
|
||||
|
||||
**Authentication:** Bearer token (KOReader device token)
|
||||
|
||||
**Request Body:**
|
||||
- `book_sha256` (string): SHA-256 hash of book file
|
||||
- `bookmarks` (array): Array of bookmarks
|
||||
- `notes` (array): Array of notes
|
||||
- `highlights` (array): Array of highlights
|
||||
|
||||
**Response:**
|
||||
- `sync_status` (string): Sync status
|
||||
- `total_synced` (number): Total items synced
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
meta {
|
||||
name: KOReader Sync Bookmarks
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/sync/koreader/bookmarks
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{device_token}}
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"book_uuid": "{{book_uuid}}",
|
||||
"bookmarks": [
|
||||
{
|
||||
"chapter": 3,
|
||||
"datetime": "2026-01-30T19:55:00Z",
|
||||
"notes": "Bookmarked text",
|
||||
"pos0": "epubcfi(/6/4/2:15)",
|
||||
"pos1": "epubcfi(/6/4/2:20)",
|
||||
"page": 45,
|
||||
"text": "This is important",
|
||||
"type": "highlight",
|
||||
"percentage": 0.45
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
{
|
||||
"chapter": 3,
|
||||
"datetime": "2026-01-30T19:55:00Z",
|
||||
"notes": "My note here",
|
||||
"pos0": "epubcfi(/6/4/2:15)",
|
||||
"page": 45,
|
||||
"text": "Note content",
|
||||
"type": "note"
|
||||
}
|
||||
],
|
||||
"highlights": [
|
||||
{
|
||||
"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",
|
||||
"color": "#ffff00",
|
||||
"percentage": 0.45
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200) {
|
||||
const body = res.getBody();
|
||||
tests['Sync completed'] = body.sync_status === "completed";
|
||||
tests('Items synced >= 0', body.total_synced >= 0);
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## KOReader Sync Bookmarks
|
||||
|
||||
Synchronizes bookmarks, notes, and highlights from a KOReader device.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/sync/koreader/bookmarks
|
||||
|
||||
**Authentication:** Bearer token (device token)
|
||||
|
||||
**Request Body:**
|
||||
- `book_uuid` (string): Book UUID
|
||||
- `bookmarks` (array): Array of bookmark objects
|
||||
- `notes` (array): Array of note objects
|
||||
- `highlights` (array): Array of highlight objects
|
||||
|
||||
Each object includes:
|
||||
- `chapter` (number): Chapter number
|
||||
- `datetime` (string): ISO 8601 timestamp
|
||||
- `text` (string): Highlighted/bookmarked text
|
||||
- `pos0`, `pos1` (string): EPUB CFI positions
|
||||
- `page` (number): Page number
|
||||
- `type` (string): Type (highlight, bookmark, note)
|
||||
- `percentage` (number): Position in book (0-1)
|
||||
|
||||
**Response:**
|
||||
- `sync_status` (string): Sync status (completed, partial)
|
||||
- `total_synced` (number): Number of items synced
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
meta {
|
||||
name: KOReader Sync Progress - SHA-256 Only
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/v1/koreader/sync/progress
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{koreader_device_token}}
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"sync_mode": "immediate",
|
||||
"books": [
|
||||
{
|
||||
"sha256": "{{book_sha256}}",
|
||||
"file_path": "/mnt/onboard/Unknown%20Book.epub",
|
||||
"percentage": 0.45,
|
||||
"page": 89,
|
||||
"total_pages": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
tests('Status is 200 or 202', res.getStatus() === 200 || res.getStatus() === 202);
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## KOReader Sync Progress - SHA-256 Only
|
||||
|
||||
Synchronizes reading progress using only SHA-256 hash for book identification (when UUID is not available).
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/v1/koreader/sync/progress
|
||||
|
||||
**Authentication:** Bearer token (KOReader device token)
|
||||
|
||||
**Request Body:**
|
||||
- `sync_mode` (string): Sync mode (immediate, deferred)
|
||||
- `books` (array): Array of book progress objects
|
||||
- `sha256` (string): SHA-256 hash of book file
|
||||
- `file_path` (string): Path to book file
|
||||
- `percentage` (number): Progress percentage
|
||||
- `page` (number): Current page
|
||||
- `total_pages` (number): Total pages
|
||||
|
||||
**Response:**
|
||||
- `sync_status` (string): Sync status
|
||||
- `books_synced` (number): Number of books synced
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 202: Accepted
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
|
||||
**Note:** Use this when book UUID is not available, falling back to SHA-256 hash for identification.
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
meta {
|
||||
name: KOReader Sync Progress - SHA-256
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/v1/koreader/sync/progress
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{koreader_device_token}}
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"sync_mode": "immediate",
|
||||
"books": [
|
||||
{
|
||||
"uuid": "{{book_uuid}}",
|
||||
"sha256": "{{book_sha256}}",
|
||||
"file_path": "/mnt/onboard/The%20Hobbit.epub",
|
||||
"percentage": 0.65,
|
||||
"chapter": 5,
|
||||
"page": 142,
|
||||
"total_pages": 310,
|
||||
"epubcfi": "/6/4[chap1ref]!/4/2/1:0",
|
||||
"last_read": "2026-01-31T12:00:00Z",
|
||||
"title": "The Hobbit",
|
||||
"authors": ["J.R.R. Tolkien"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 200 || res.getStatus() === 202) {
|
||||
const body = res.getBody();
|
||||
tests['Status accepted'] = res.getStatus() === 200 || res.getStatus() === 202;
|
||||
tests('Has sync_status', body.sync_status !== undefined);
|
||||
tests('Books synced >= 0', body.books_synced >= 0);
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## KOReader Sync Progress - SHA-256
|
||||
|
||||
Synchronizes reading progress from a KOReader device using SHA-256 book hash for identification.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/v1/koreader/sync/progress
|
||||
|
||||
**Authentication:** Bearer token (KOReader device token)
|
||||
|
||||
**Request Body:**
|
||||
- `sync_mode` (string): Sync mode (immediate, deferred)
|
||||
- `books` (array): Array of book progress objects
|
||||
- `uuid` (string): Book UUID
|
||||
- `sha256` (string): SHA-256 hash of book file for identification
|
||||
- `file_path` (string): Path to book file on device
|
||||
- `percentage` (number): Progress percentage (0-1)
|
||||
- `chapter` (number): Current chapter
|
||||
- `page` (number): Current page
|
||||
- `total_pages` (number): Total pages
|
||||
- `epubcfi` (string): EPUB location
|
||||
- `last_read` (string): ISO 8601 timestamp
|
||||
- `title` (string): Book title
|
||||
- `authors` (array): List of authors
|
||||
|
||||
**Response:**
|
||||
- `sync_status` (string): Sync status
|
||||
- `books_synced` (number): Number of books synced
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 202: Accepted - processing
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
meta {
|
||||
name: KOReader Sync Progress
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/sync/koreader/progress
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{device_token}}
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"library_id": null,
|
||||
"books": [
|
||||
{
|
||||
"uuid": "{{book_uuid}}",
|
||||
"title": "Test Book",
|
||||
"authors": ["Test Author"],
|
||||
"progress": 0.45,
|
||||
"percentage": 0.45,
|
||||
"last_read": "2026-01-30T20:00:00Z",
|
||||
"chapter": 5,
|
||||
"character": 15432,
|
||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
||||
"page": 89,
|
||||
"total_pages": 200
|
||||
}
|
||||
],
|
||||
"sync_mode": "immediate",
|
||||
"device_info": {
|
||||
"koreader_version": "2024.01",
|
||||
"device_model": "kindle-paperwhite-5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
if (res.getStatus() === 202) {
|
||||
const body = res.getBody();
|
||||
tests['Status is 202'] = res.getStatus() === 202;
|
||||
tests['Sync status accepted'] = body.sync_status === "accepted";
|
||||
tests('Books synced >= 0', body.books_synced >= 0);
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## KOReader Sync Progress
|
||||
|
||||
Synchronizes reading progress from a KOReader device to the Bookhoard server.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/sync/koreader/progress
|
||||
|
||||
**Authentication:** Bearer token (device token)
|
||||
|
||||
**Request Body:**
|
||||
- `library_id` (string, optional): Library UUID
|
||||
- `books` (array): Array of book progress objects
|
||||
- `uuid` (string): Book UUID
|
||||
- `title` (string): Book title
|
||||
- `authors` (array): List of authors
|
||||
- `progress` (number): Progress value
|
||||
- `percentage` (number): Percentage complete (0-1)
|
||||
- `last_read` (string): ISO 8601 timestamp
|
||||
- `chapter` (number): Current chapter
|
||||
- `epubcfi` (string): EPUB Canonical Fragment Identifier
|
||||
- `page` (number): Current page
|
||||
- `total_pages` (number): Total pages
|
||||
- `sync_mode` (string): Sync mode (immediate, deferred)
|
||||
- `device_info` (object): Device information
|
||||
- `koreader_version` (string): KOReader version
|
||||
- `device_model` (string): Device model identifier
|
||||
|
||||
**Response:**
|
||||
- `sync_status` (string): Sync status (accepted, processing)
|
||||
- `books_synced` (number): Number of books synced
|
||||
|
||||
**Status Codes:**
|
||||
- 202: Accepted - sync queued
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
meta {
|
||||
name: Add Library Folder
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/libraries/{{library_id}}/folders
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"folder_path": {{library_folder}}
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
libraryId: "8821b703-h29b-71d4-d716-446655440003"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Add Library Folder
|
||||
|
||||
Adds a new folder path to a library for media scanning and indexing.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/libraries/{id}/folders
|
||||
|
||||
**Authentication:** Required (Bearer token, admin permissions)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string, required): Library UUID
|
||||
|
||||
**Request Body:**
|
||||
- `folder_path` (string, required): Absolute path to the folder containing media files
|
||||
|
||||
**Response:** Folder object
|
||||
- `id` (string): Folder UUID
|
||||
- `library_id` (string): Library UUID
|
||||
- `folder_path` (string): Absolute path to folder
|
||||
- `is_active` (boolean): Whether folder is active for scanning
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 201: Folder added successfully
|
||||
- 400: Invalid folder path or request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
- 404: Library not found
|
||||
- 409: Folder already exists for this library
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
meta {
|
||||
name: Create Library
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/libraries
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"name": "My Ebook Library",
|
||||
"description": "A collection of technical books and novels",
|
||||
"type": "ebooks"
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
function onResponse(res) {
|
||||
let data = res.getBody();
|
||||
// If successful registration, set token environment variable
|
||||
if (res.getStatus() === 201 || res.getStatus() === 200) {
|
||||
if (data && data.id) {
|
||||
|
||||
return bru.setEnvVar("library_id", data.id, { persist: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
onResponse(res);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Create Library
|
||||
|
||||
Creates a new library for organizing media items.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/libraries
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Request Body:**
|
||||
- `name` (string, required): Library name
|
||||
- `description` (string, optional): Library description
|
||||
- `type` (string, required): Library type
|
||||
- `"ebooks"`: Electronic books
|
||||
- `"audiobooks"`: Audio books
|
||||
- `"videos"`: Video content
|
||||
- `"other"`: Other media types
|
||||
|
||||
**Response:** Library object
|
||||
- `id` (string): Library UUID
|
||||
- `name` (string): Library name
|
||||
- `description` (string, optional): Library description
|
||||
- `type` (string): Library type
|
||||
- `is_visible` (boolean): Library visibility status
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 201: Library created successfully
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (insufficient permissions)
|
||||
- 409: Library name already exists
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
meta {
|
||||
name: Delete Library Folder
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{base_url}}/api/libraries/{{library_id}}/folders
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
"folder_path": "/path/to/folder"
|
||||
}
|
||||
|
||||
tests {
|
||||
test_delete_library_folder_success(status, headers, body) {
|
||||
if (status !== 204) {
|
||||
throw new Error("Expected status 204, got " + status);
|
||||
}
|
||||
|
||||
// Delete should return no content
|
||||
if (body && body.length > 0) {
|
||||
throw new Error("Expected empty response body for delete");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
libraryId: "cc23c3a7-f8fb-451a-a78d-2a16df1b725a"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Delete Library Folder
|
||||
|
||||
Removes a folder from a library's scanning configuration.
|
||||
|
||||
**Method:** DELETE
|
||||
|
||||
**Endpoint:** /api/libraries/{id}/folders
|
||||
**Authentication:** Required (Bearer token, admin only)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Library UUID
|
||||
|
||||
**Request Body:**
|
||||
- `folder_path` (string, required): Path to folder to remove
|
||||
|
||||
**Response:** Empty (204 No Content)
|
||||
|
||||
**Status Codes:**
|
||||
- 204: Folder deleted successfully
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
- 404: Library not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Examples:**
|
||||
- Delete folder: `DELETE /api/libraries/cc23c3a7-f8fb-451a-a78d-2a16df1b725a/folders`
|
||||
|
||||
**Note:** Admin access required - only users with admin role can manage library folders.
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
meta {
|
||||
name: Delete Library
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{base_url}}/api/libraries/{{library_id}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_delete_library_success(status, headers, body) {
|
||||
if (status !== 204) {
|
||||
throw new Error("Expected status 204, got " + status);
|
||||
}
|
||||
|
||||
// Delete should return no content
|
||||
if (body && body.length > 0) {
|
||||
throw new Error("Expected empty response body for delete");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
libraryId: "cc23c3a7-f8fb-451a-a78d-2a16df1b725a"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Delete Library
|
||||
|
||||
Deletes a library and all associated media items.
|
||||
|
||||
**Method:** DELETE
|
||||
|
||||
**Endpoint:** /api/libraries/{id}
|
||||
|
||||
**Authentication:** Required (Bearer token, admin only)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Library UUID
|
||||
|
||||
**Response:** Empty (204 No Content)
|
||||
|
||||
**Status Codes:**
|
||||
- 204: Library deleted successfully
|
||||
- 400: Invalid library ID
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
- 404: Library not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Warning:** This will delete ALL media items in the library.
|
||||
|
||||
**Examples:**
|
||||
- Delete library: `DELETE /api/libraries/cc23c3a7-f8fb-451a-a78d-2a16df1b725a`
|
||||
|
||||
**Note:** Admin access required - only users with admin role can delete libraries.
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
meta {
|
||||
name: Get Libraries (Admin)
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/libraries
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Libraries (Admin)
|
||||
|
||||
Retrieves all libraries in the system (admin access required).
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/libraries
|
||||
|
||||
**Authentication:** Required (Bearer token, admin permissions)
|
||||
|
||||
**Response:** Array of library objects
|
||||
- `id` (string): Library UUID
|
||||
- `name` (string): Library name
|
||||
- `description` (string, optional): Library description
|
||||
- `type` (string): Library type (ebooks, audiobooks, videos, other)
|
||||
- `is_visible` (boolean): Library visibility to users
|
||||
- `media_count` (number): Number of media items in library
|
||||
- `folder_count` (number): Number of folders associated
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
meta {
|
||||
name: Get Library Folders
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/libraries/{{library_id}}/folders
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
libraryId: "8821b703-h29b-71d4-d716-446655440003"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Library Folders
|
||||
|
||||
Retrieves all folders associated with a specific library.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/libraries/{id}/folders
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string, required): Library UUID
|
||||
|
||||
**Response:** Array of folder objects
|
||||
- `id` (string): Folder UUID
|
||||
- `library_id` (string): Library UUID
|
||||
- `folder_path` (string): Absolute path to the folder
|
||||
- `is_active` (boolean): Whether the folder is currently active for scanning
|
||||
- `last_scanned` (string, optional): Timestamp of last scan
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (library access denied)
|
||||
- 404: Library not found
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
meta {
|
||||
name: Get Library Stats
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/libraries/{{library_id}}/stats
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_get_library_stats_success(status, headers, body) {
|
||||
if (status !== 200) {
|
||||
throw new Error("Expected status 200, got " + status);
|
||||
}
|
||||
|
||||
const contentType = headers["content-type"];
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
throw new Error("Expected content-type to contain application/json, got " + contentType);
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(body);
|
||||
} catch (e) {
|
||||
throw new Error("Response body is not valid JSON");
|
||||
}
|
||||
|
||||
if (!data || typeof data !== "object") {
|
||||
throw new Error("Expected stats object in response");
|
||||
}
|
||||
|
||||
// Expected fields (verify they exist)
|
||||
const expectedFields = ["total_items", "total_size", "scanned_at"];
|
||||
for (const field of expectedFields) {
|
||||
if (!(field in data)) {
|
||||
console.warn("Stats field '" + field + "' is missing from response");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
libraryId: "cc23d3a7-f8fb-451a-a78d-2a16df1b725a"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Library Stats
|
||||
|
||||
Retrieves statistical information about a library's media items.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/libraries/{id}/stats
|
||||
|
||||
**Authentication:** Required (Bearer token, admin only)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Library UUID
|
||||
|
||||
**Response:** Stats object
|
||||
- `total_items` (number): Total media items in library
|
||||
- `total_size` (number): Total file size in bytes
|
||||
- `scanned_at` (string): Last scan timestamp
|
||||
- Additional fields may be included
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid library ID
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
- 404: Library not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Examples:**
|
||||
- Get library stats: `GET /api/libraries/cc23c3a7-f8fb-451a-a78d-2a16df1b725a/stats`
|
||||
|
||||
**Note:** Admin access required - only users with admin role can access library statistics.
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
meta {
|
||||
name: Get Library Types
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/libraries/types
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Library Types
|
||||
|
||||
Retrieves all available library types that can be used when creating libraries.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/libraries/types
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Response:** Array of library type objects
|
||||
- `id` (string): Type identifier (e.g., "ebooks", "audiobooks", "videos", "other")
|
||||
- `name` (string): Display name for the type (e.g., "Ebooks", "Audiobooks", "Videos", "Other")
|
||||
- `description` (string, optional): Description of what this type is used for
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
meta {
|
||||
name: Get Library
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/libraries/{{library_id}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_get_library_success(status, headers, body) {
|
||||
if (status !== 200) {
|
||||
throw new Error("Expected status 200, got " + status);
|
||||
}
|
||||
|
||||
const contentType = headers["content-type"];
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
throw new Error("Expected content-type to contain application/json, got " + contentType);
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(body);
|
||||
} catch (e) {
|
||||
throw new Error("Response body is not valid JSON");
|
||||
}
|
||||
|
||||
// Verify library object structure
|
||||
if (!data || typeof data !== "object") {
|
||||
throw new Error("Expected library object in response");
|
||||
}
|
||||
|
||||
// Required fields
|
||||
if (!data.id || !data.name || !data.library_type_id) {
|
||||
throw new Error("Library missing required fields: id, name, library_type_id");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
libraryId: "cc23c3a7-f8fb-451a-a78d-2a16df1b725a"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Library
|
||||
|
||||
Retrieves detailed information about a specific library by ID.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/libraries/{id}
|
||||
|
||||
**Authentication:** Required (Bearer token, admin only)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Library UUID
|
||||
|
||||
**Response:** Library object
|
||||
- `id` (string): Library UUID
|
||||
- `name` (string): Library name
|
||||
- `description` (string): Library description
|
||||
- `library_type_id` (string): Library type UUID
|
||||
- `created_by_admin_id` (string): Admin UUID who created it
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
- 404: Library not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Examples:**
|
||||
- Get library: `GET /api/libraries/cc23c3a7-f8fb-451a-a78d-2a16df1b725a`
|
||||
|
||||
**Note:** Admin access required - only users with admin role can access library details.
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
meta {
|
||||
name: Get Scan Settings
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/library/scan-settings
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Scan Settings
|
||||
|
||||
Retrieves the user's current library scanning settings.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/library/scan-settings
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Response:**
|
||||
- `scan_frequency_minutes` (number): Minutes between automatic scans (minimum 1)
|
||||
- `auto_scan_enabled` (boolean): Whether automatic scanning is enabled
|
||||
- `last_scan_at` (string, optional): Timestamp of last scan
|
||||
- `next_scan_at` (string, optional): Timestamp of next scheduled scan
|
||||
- `library_id` (string, optional): Library ID for context
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (access denied)
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
meta {
|
||||
name: Get User Visible Libraries
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/libraries/visible
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get User Visible Libraries
|
||||
|
||||
Retrieves all libraries that are visible to regular users.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/libraries/visible
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Response:** Array of visible library objects
|
||||
- `id` (string): Library UUID
|
||||
- `name` (string): Library name
|
||||
- `description` (string, optional): Library description
|
||||
- `type` (string): Library type (ebooks, audiobooks, videos, other)
|
||||
- `is_visible` (boolean): Always true for this endpoint
|
||||
- `media_count` (number): Number of media items in library
|
||||
- `created_at` (string): Creation timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
meta {
|
||||
name: Set Library Visibility
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/libraries/visibility
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"library_id": "{{library_id}}",
|
||||
"is_visible": {{is_visible}}
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
libraryId: "8821b703-h29b-71d4-d716-446655440003",
|
||||
isVisible: true
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Set Library Visibility
|
||||
|
||||
Updates the visibility status of a library for regular users.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/libraries/visibility
|
||||
|
||||
**Authentication:** Required (Bearer token, admin permissions)
|
||||
|
||||
**Request Body:**
|
||||
- `library_id` (string, required): Library UUID
|
||||
- `is_visible` (boolean, required): Visibility status
|
||||
- `true`: Library visible to all users
|
||||
- `false`: Library hidden from regular users
|
||||
|
||||
**Response:** Updated library visibility object
|
||||
- `library_id` (string): Library UUID
|
||||
- `is_visible` (boolean): Updated visibility status
|
||||
- `updated_at` (string): Update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Visibility updated successfully
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
- 404: Library not found
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
meta {
|
||||
name: Update Library
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{base_url}}/api/libraries/{{library_id}}
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
"name": "Updated Library Name",
|
||||
"description": "Updated library description"
|
||||
}
|
||||
|
||||
tests {
|
||||
test_update_library_success(status, headers, body) {
|
||||
if (status !== 200) {
|
||||
throw new Error("Expected status 200, got " + status);
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(body);
|
||||
} catch (e) {
|
||||
throw new Error("Response body is not valid JSON");
|
||||
}
|
||||
|
||||
if (!data || typeof data !== "object") {
|
||||
throw new Error("Expected updated library object in response");
|
||||
}
|
||||
|
||||
if (!data.id || !data.updated_at) {
|
||||
throw new Error("Library response missing update confirmation");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
libraryId: "cc23c3a7-f8fb-451a-a78d-2a16df1b725a"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Update Library
|
||||
|
||||
Updates an existing library's information.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/libraries/{id}
|
||||
|
||||
**Authentication:** Required (Bearer token, admin only)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Library UUID
|
||||
|
||||
**Request Body:**
|
||||
- `name` (string, optional): Library name
|
||||
- `description` (string, optional): Library description
|
||||
|
||||
**Response:** Updated library object
|
||||
- `id` (string): Library UUID
|
||||
- `name` (string): Updated library name
|
||||
- `description` (string): Updated library description
|
||||
- `library_type_id` (string): Library type UUID
|
||||
- `updated_at` (string): Update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
- 404: Library not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Examples:**
|
||||
- Update library: `PUT /api/libraries/cc23c3a7-f8fb-451a-a78d-2a16df1b725a`
|
||||
|
||||
**Note:** Admin access required - only users with admin role can update libraries.
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
meta {
|
||||
name: Update Scan Settings
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{base_url}}/api/library/scan-settings
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"scan_frequency_minutes": 60,
|
||||
"auto_scan_enabled": true
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Update Scan Settings
|
||||
|
||||
Updates the user's media scanning settings.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/library/scan-settings
|
||||
|
||||
**Authentication:** Required
|
||||
|
||||
**Request Body:**
|
||||
- `scan_frequency_minutes` (integer, required): Minutes between automatic scans (15-1440)
|
||||
- `auto_scan_enabled` (boolean, required): Whether automatic scanning is enabled
|
||||
|
||||
**Response:**
|
||||
- `message` (string): Success message
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid settings
|
||||
- 401: Unauthorized
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
meta {
|
||||
name: Create Media Item
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/media-items
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
"library_id": "{{library_id}}",
|
||||
"title": "New Media Item",
|
||||
"author": "Author Name",
|
||||
"isbn": "978-0123456789",
|
||||
"description": "Description of the media item",
|
||||
"cover_image_path": "/path/to/cover.jpg",
|
||||
"series": "Series Name",
|
||||
"series_number": 1,
|
||||
"tags": ["science fiction", "ACME CORP.", "non-fiction"],
|
||||
"asin": "B08XYZ123",
|
||||
"date_published": "2023-01-15",
|
||||
"publisher": "Publisher Name",
|
||||
"contributors": ["O'Reilly Media", "acme corp"],
|
||||
"language": "en",
|
||||
"edition": "First Edition",
|
||||
"page_count": 350,
|
||||
"genre": "Science Fiction",
|
||||
"copyright_year": 2023,
|
||||
"goodreads_id": "123456",
|
||||
"openlibrary_id": "OL123456M",
|
||||
"google_books_id": "GB123456"
|
||||
}
|
||||
|
||||
tests {
|
||||
test_create_media_item_success(status, headers, body) {
|
||||
if (status !== 201) {
|
||||
throw new Error("Expected status 201, got " + status);
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(body);
|
||||
} catch (e) {
|
||||
throw new Error("Response body is not valid JSON");
|
||||
}
|
||||
|
||||
if (!data || typeof data !== "object") {
|
||||
throw new Error("Expected media item object in response");
|
||||
}
|
||||
|
||||
// Verify required fields
|
||||
if (!data.id || !data.title || !data.library_id) {
|
||||
throw new Error("Media item missing required fields: id, title, library_id");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
test_create_media_item_no_folders(status, headers, body) {
|
||||
if (status !== 400) {
|
||||
throw new Error("Expected status 400 for library with no folders, got " + status);
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(body);
|
||||
} catch (e) {
|
||||
throw new Error("Response body is not valid JSON");
|
||||
}
|
||||
|
||||
if (!data || typeof data !== 'object') {
|
||||
throw new Error("Expected error object in response");
|
||||
}
|
||||
|
||||
if (!data.error || typeof data.error !== 'string') {
|
||||
throw new Error("Expected error message in response");
|
||||
}
|
||||
|
||||
if (!data.error.includes("folder")) {
|
||||
throw new Error("Error message should mention folders requirement");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
libraryId: "cc23c3a7-f8fb-451a-a78d-2a16df1b725a"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Create Media Item
|
||||
|
||||
Creates a new media item in a library with full metadata.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/media-items
|
||||
|
||||
**Authentication:** Required (Bearer token, admin only)
|
||||
|
||||
**Prerequisites**
|
||||
- Library must have at least one folder configured before media items can be added
|
||||
- Use `POST /api/libraries/{library_id}/folders` to add folders first
|
||||
- See: [Add Library Folder](../../library/Add%20Library%20Folder.bru)
|
||||
|
||||
**Request Body:**
|
||||
- `library_id` (string, required): Library UUID
|
||||
- `title` (string, required): Media item title
|
||||
- `author` (string, optional): Author name
|
||||
- `isbn` (string, optional): ISBN number
|
||||
- `description` (string, optional): Description
|
||||
- `cover_image_path` (string, optional): Path to cover image
|
||||
- `series` (string, optional): Series name
|
||||
- `series_number` (integer, optional): Number in series
|
||||
- `tags` (array of string, optional): Tags or categories (auto-normalized) (automatically normalized)
|
||||
- `asin` (string, optional): Amazon ASIN
|
||||
- `date_published` (string, optional): Publication date
|
||||
- `publisher` (string, optional): Publisher name
|
||||
- `contributors` (array of string, optional): List of contributors (auto-normalized) (automatically normalized)
|
||||
- `language` (string, optional): Language code (ISO 639-1)
|
||||
- `edition` (string, optional): Edition information
|
||||
- `page_count` (integer, optional): Total page count
|
||||
- `genre` (string, optional): Genre classification
|
||||
- `copyright_year` (integer, optional): Copyright year
|
||||
- `goodreads_id` (string, optional): Goodreads identifier
|
||||
- `openlibrary_id` (string, optional): Open Library identifier
|
||||
- `google_books_id` (string, optional): Google Books identifier
|
||||
|
||||
**Response:** Created media item object
|
||||
- All fields above plus:
|
||||
- `tags_search` (array): Normalized for search (lowercase, no punctuation)
|
||||
- `contributors_search` (array): Normalized for search (lowercase, no punctuation)
|
||||
|
||||
**Normalization Behavior:**
|
||||
|
||||
Tags are automatically normalized:
|
||||
- Trim whitespace
|
||||
- Titlecased (preserves hyphenation: "non-fiction" → "Non-Fiction")
|
||||
- Case-insensitive deduplication (keeps version with punctuation if exists)
|
||||
- Example: `["science fiction", "SCIENCE-FICTION"]` → `["Science-Fiction"]`
|
||||
|
||||
Contributors are automatically normalized:
|
||||
- Trim whitespace
|
||||
- Preserve original casing (CAPSLOCK companies, Title Case, etc.)
|
||||
- Preserve punctuation for display
|
||||
- Case-insensitive deduplication (keeps version with punctuation if exists)
|
||||
- Example: `["acme corp", "ACME CORP.", "acme corp"]` → `["ACME CORP."]`
|
||||
|
||||
**Validation Error Response**
|
||||
- **400 Bad Request** - Library has no folders:
|
||||
```json
|
||||
{
|
||||
"error": "Cannot add media items to a library with no folders. Please add at least one folder to the library first."
|
||||
}
|
||||
```
|
||||
|
||||
**Status Codes:**
|
||||
- 201: Media item created successfully
|
||||
- 400: Invalid request data OR library has no folders
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
- 404: Library not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Setup Workflow Example:**
|
||||
```bash
|
||||
# 1. Create library
|
||||
POST /api/libraries
|
||||
{ "name": "My Books", "type": "ebooks" }
|
||||
|
||||
# 2. Add folder to library (REQUIRED before adding media items)
|
||||
POST /api/libraries/{library_id}/folders
|
||||
{ "folder_path": "/mnt/books/my-library" }
|
||||
|
||||
# 3. Create media items (now that library has folders)
|
||||
POST /api/media-items
|
||||
{ "library_id": "...", "title": "Book Title", ... }
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- Create media item: `POST /api/media-items`
|
||||
|
||||
**Note:** Admin access required - only users with admin role can create media items.
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
meta {
|
||||
name: Create Media Rating
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/media-items/{{media_item_id}}/rating
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"rating": 8
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
mediaItemId: "9932c704-i29b-81d4-e716-446655440004"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Create Media Rating
|
||||
|
||||
Creates or updates the authenticated user's rating for a specific media item.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/media-items/{id}/rating
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string, required): Media item UUID
|
||||
|
||||
**Request Body:**
|
||||
- `rating` (number, required): Rating value (1-10, where odd numbers = half-stars)
|
||||
- 1,3,5,7,9 = 0.5,1.5,2.5,3.5,4.5 stars (half-star precision)
|
||||
- 2,4,6,8,10 = 1,2,3,4,5 stars (full stars)
|
||||
|
||||
**Example Request:**
|
||||
- `"rating": 7` = 3.5 stars (frontend display)
|
||||
- `"rating": 8` = 4.0 stars (frontend display)
|
||||
|
||||
**Response:** Rating object
|
||||
- `id` (string): Rating UUID
|
||||
- `media_item_id` (string): Media item UUID
|
||||
- `user_id` (string): User UUID
|
||||
- `rating` (number): Rating value (1-10)
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 201: Rating created successfully
|
||||
- 200: Rating updated successfully (if rating already existed)
|
||||
- 400: Invalid rating value (must be 1-10)
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (rating access denied)
|
||||
- 404: Media item not found
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
meta {
|
||||
name: Delete Media Item
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{base_url}}/api/media-items/{{media_item_id}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_delete_media_item_success(status, headers, body) {
|
||||
if (status !== 204) {
|
||||
throw new Error("Expected status 204, got " + status);
|
||||
}
|
||||
|
||||
// Delete should return no content
|
||||
if (body && body.length > 0) {
|
||||
throw new Error("Expected empty response body for delete");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
mediaItemId: "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Delete Media Item
|
||||
|
||||
Deletes a media item from the library.
|
||||
|
||||
**Method:** DELETE
|
||||
|
||||
**Endpoint:** /api/media-items/{id}
|
||||
|
||||
**Authentication:** Required (Bearer token, admin only)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Media item UUID
|
||||
|
||||
**Response:** Empty (204 No Content)
|
||||
|
||||
**Status Codes:**
|
||||
- 204: Media item deleted successfully
|
||||
- 400: Invalid media item ID
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
- 404: Media item not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Examples:**
|
||||
- Delete media item: `DELETE /api/media-items/550e8400-e29b-41d4-a716-446655440000`
|
||||
|
||||
**Warning:** This permanently removes the media item and all associated data (ratings, notes, highlights).
|
||||
|
||||
**Note:** Admin access required - only users with admin role can delete media items.
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
meta {
|
||||
name: Delete Media Rating
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{base_url}}/api/media-items/{{media_item_id}}/rating
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_delete_media_rating_success(status, headers, body) {
|
||||
if (status !== 204) {
|
||||
throw new Error("Expected status 204, got " + status);
|
||||
}
|
||||
|
||||
// Delete should return no content
|
||||
if (body && body.length > 0) {
|
||||
throw new Error("Expected empty response body for delete");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
mediaItemId: "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Delete Media Rating
|
||||
|
||||
Deletes a user's rating for a specific media item.
|
||||
|
||||
**Method:** DELETE
|
||||
|
||||
**Endpoint:** /api/media-items/{id}/rating
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Media item UUID
|
||||
|
||||
**Response:** Empty (204 No Content)
|
||||
|
||||
**Status Codes:**
|
||||
- 204: Rating deleted successfully
|
||||
- 401: Unauthorized
|
||||
- 404: Media item not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Examples:**
|
||||
- Delete media rating: `DELETE /api/media-items/550e8400-e29b-41d4-a716-446655440000/rating`
|
||||
|
||||
**Note:** Deletes only the authenticated user's rating, not other users' ratings.
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
meta {
|
||||
name: Download Media Item
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{baseURL}}/api/media-items/{{bookUUID}}/download
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Download Media Item
|
||||
|
||||
Download a media item file (EPUB, PDF, etc.) from Bookhoard server.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/media-items/{bookUUID}/download
|
||||
|
||||
**Authentication:** None (for Kobo device downloads)
|
||||
|
||||
**Path Parameters:**
|
||||
- `bookUUID` (string): Media item UUID
|
||||
|
||||
**Response:** Binary file data (EPUB, PDF, etc.)
|
||||
|
||||
**Response Headers:**
|
||||
- `Content-Type`: application/epub+zip, application/pdf, or appropriate MIME type
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success - File returned
|
||||
- 404: Media item not found
|
||||
|
||||
**Note:** This endpoint is designed for Kobo devices to download media items directly from Bookhoard. The endpoint returns the file with appropriate Content-Type headers.
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
meta {
|
||||
name: Filter Media Items
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/media-items/filtered?library_id={{library_id}}&genre_filter=Fiction&language_filter=en&year_min=2000&year_max=2024&limit=10&offset=0
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
genre: "Fiction"
|
||||
language: "en"
|
||||
yearMin: 2000
|
||||
yearMax: 2024
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Filter Media Items
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/media-items/filtered
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Query Parameters:**
|
||||
- `library_id` (string, required): UUID of the library
|
||||
- `author_filter` (string, optional): Filter by author (partial match)
|
||||
- `series_filter` (string, optional): Filter by series (partial match)
|
||||
- `genre_filter` (string, optional): Filter by genre (exact match)
|
||||
- `language_filter` (string, optional): Filter by language (exact match, e.g., 'en', 'es', 'fr')
|
||||
- `year_min` (integer, optional): Minimum copyright year
|
||||
- `year_max` (integer, optional): Maximum copyright year
|
||||
- `has_cover` (boolean, optional): Filter for items with cover images only
|
||||
- `sort` (string, optional): Sort field and direction (same options as ListMediaItems)
|
||||
- `limit` (integer, optional): Number of items to return (default: 50, max: 1000)
|
||||
- `offset` (integer, optional): Number of items to skip (default: 0)
|
||||
|
||||
**Response:** Object containing array of filtered media items
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Bad request (invalid parameters)
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
|
||||
**Examples:**
|
||||
- Filter by genre: `/api/media-items/filtered?library_id=xxx&genre_filter=Fiction`
|
||||
- Filter by language: `/api/media-items/filtered?library_id=xxx&language_filter=es`
|
||||
- Filter by year range: `/api/media-items/filtered?library_id=xxx&year_min=2000&year_max=2024`
|
||||
- Filter by cover: `/api/media-items/filtered?library_id=xxx&has_cover=true`
|
||||
- Combine filters: `/api/media-items/filtered?library_id=xxx&genre_filter=Sci-Fi&year_min=2010&language_filter=en`
|
||||
|
||||
**Filter Behavior:**
|
||||
- Multiple filters can be combined (AND logic)
|
||||
- Author and series filters use partial matching (ILIKE)
|
||||
- Genre and language filters use exact matching
|
||||
- Year range filters are inclusive
|
||||
- Filters are applied before sorting and pagination
|
||||
- User library visibility is respected
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
meta {
|
||||
name: Get Media Item
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/media-items/{{media_item_id}}
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_get_media_item_success(status, headers, body) {
|
||||
if (status !== 200) {
|
||||
throw new Error("Expected status 200, got " + status);
|
||||
}
|
||||
|
||||
const contentType = headers["content-type"];
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
throw new Error("Expected content-type to contain application/json, got " + contentType);
|
||||
}
|
||||
|
||||
// Verify response body is valid JSON and has expected structure
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(body);
|
||||
} catch (e) {
|
||||
throw new Error("Response body is not valid JSON");
|
||||
}
|
||||
|
||||
if (!data || typeof data !== "object") {
|
||||
throw new Error("Expected response body to be an object");
|
||||
}
|
||||
|
||||
// Check for required fields in media item
|
||||
if (!data.id) {
|
||||
throw new Error("Media item missing required field: id");
|
||||
}
|
||||
|
||||
if (!data.title) {
|
||||
throw new Error("Media item missing required field: title");
|
||||
}
|
||||
|
||||
if (!data.library_id) {
|
||||
throw new Error("Media item missing required field: library_id");
|
||||
}
|
||||
|
||||
if (!data.media_type) {
|
||||
throw new Error("Media item missing required field: media_type");
|
||||
}
|
||||
|
||||
// Validate data types
|
||||
if (typeof data.id !== "string") {
|
||||
throw new Error("Media item id must be a string");
|
||||
}
|
||||
|
||||
if (typeof data.title !== "string") {
|
||||
throw new Error("Media item title must be a string");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
mediaItemId: "9932c704-i29b-81d4-e716-446655440004"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Media Item
|
||||
|
||||
Retrieves detailed information about a specific media item.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/media-items/{id}
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string, required): Media item UUID
|
||||
|
||||
**Response:** Media item object
|
||||
- `id` (string): Media item UUID
|
||||
- `title` (string): Media item title
|
||||
- `description` (string, optional): Media description
|
||||
- `library_id` (string): Library UUID
|
||||
- `media_type` (string): Type of media (e.g., "ebook", "audiobook")
|
||||
- `file_path` (string): Path to media file
|
||||
- `file_size` (number, optional): File size in bytes
|
||||
- `metadata` (object, optional): Additional media metadata
|
||||
- `author` (string, optional): Author name (for books)
|
||||
- `isbn` (string, optional): ISBN number
|
||||
- `duration` (number, optional): Duration in seconds (for audiobooks)
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (access denied)
|
||||
- 404: Media item not found
|
||||
- 500: Internal server error
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
meta {
|
||||
name: Get Media Rating
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/media-items/{{media_item_id}}/rating
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_get_media_rating_success(status, headers, body) {
|
||||
if (status !== 200) {
|
||||
throw new Error("Expected status 200, got " + status);
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(body);
|
||||
} catch (e) {
|
||||
throw new Error("Response body is not valid JSON");
|
||||
}
|
||||
|
||||
if (!data || typeof data !== "object") {
|
||||
throw new Error("Expected rating object in response");
|
||||
}
|
||||
|
||||
// Verify expected fields
|
||||
if (!data.media_item_id || !data.rating === undefined) {
|
||||
throw new Error("Rating response missing required fields: media_item_id, rating");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
mediaItemId: "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Media Rating
|
||||
|
||||
Retrieves a user's rating for a specific media item.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/media-items/{id}/rating
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Media item UUID
|
||||
|
||||
**Response:** Rating object
|
||||
- `id` (string): Rating UUID
|
||||
- `media_item_id` (string): Media item UUID
|
||||
- `user_id` (string): User UUID
|
||||
- `rating` (integer): Rating value (1-10 scale)
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 404: Media item not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Examples:**
|
||||
- Get media rating: `GET /api/media-items/550e8400-e29b-41d4-a716-446655440000/rating`
|
||||
|
||||
**Note:** Returns the authenticated user's rating for the specified media item.
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user