refactor: update Bruno API collection for media-items system
- Remove all ebook-specific API requests (15 files deleted) - Rename Scan Ebooks.bru to Scan Media Items.bru - Update API paths from /api/ebooks to /api/media-items - Update base URL and environment configuration - Maintain all existing media-items, library, auth, and progress tests Aligns Bruno collection with unified media-items API architecture
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
meta {
|
||||
name: Add Ebook Folder
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/auth/ebook-folders
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"folder_path": "/app/uploads"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Add Ebook Folder - DEPRECATED
|
||||
|
||||
⚠️ **This endpoint is deprecated and will return HTTP 410 Gone.**
|
||||
|
||||
**Use new library system instead:**
|
||||
1. Create a library: `POST /api/libraries` with type "ebooks"
|
||||
2. Add folder to library: `POST /api/libraries/{id}/folders`
|
||||
3. Manage library visibility: `POST /api/libraries/visibility`
|
||||
|
||||
**Migration Example:**
|
||||
Old: POST `/api/auth/ebook-folders` with `{ "folder_path": "/path/to/books" }`
|
||||
New: POST `/api/libraries` with `{ "name": "My Books", "type": "ebooks" }`
|
||||
Then POST `/api/libraries/{library-id}/folders` with `{ "folder_path": "/path/to/books" }`
|
||||
|
||||
**Response:** HTTP 410 Gone with deprecation message
|
||||
|
||||
**Status Codes:**
|
||||
- 410: Gone (endpoint deprecated)
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
|
||||
**Replacement Endpoints:**
|
||||
- `POST /api/libraries` - Create new library
|
||||
- `POST /api/libraries/{id}/folders` - Add folder to library
|
||||
- `GET /api/libraries` - List all libraries (admin)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
meta {
|
||||
name: Delete Ebook Folder
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{base_url}}/api/auth/ebook-folders
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"folder_path": "/path/to/ebooks"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Delete Ebook Folder - DEPRECATED
|
||||
|
||||
⚠️ **This endpoint is deprecated and will return HTTP 410 Gone.**
|
||||
|
||||
**Method:** DELETE
|
||||
|
||||
**Endpoint:** /api/auth/ebook-folders
|
||||
|
||||
**Authentication:** Required (Admin only)
|
||||
|
||||
**Request Body:** JSON object with:
|
||||
- `folder_path` (string, required): Path to the ebook folder to delete
|
||||
|
||||
**Response:** Success message object:
|
||||
- `message` (string): "ebook folder deleted successfully"
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Folder deleted successfully
|
||||
- 400: Bad request (invalid folder path)
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
- 404: Folder not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Features:**
|
||||
- Admin-only endpoint for deleting ebook folders
|
||||
- Path normalization for matching
|
||||
- Supports both absolute paths and home directory (~) paths
|
||||
- Prevents deletion of non-existent folders
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
meta {
|
||||
name: Get Ebook Folders
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/auth/ebook-folders
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Ebook Folders - DEPRECATED
|
||||
|
||||
⚠️ **This endpoint is deprecated and will return HTTP 410 Gone.**
|
||||
|
||||
**Use the new library system instead:**
|
||||
- Use `/api/libraries` to manage libraries
|
||||
- Use `/api/libraries/{id}/folders` to manage library folders
|
||||
|
||||
**Migration Path:**
|
||||
1. Create a library of type "ebooks"
|
||||
2. Add your ebook folders to that library
|
||||
3. Use library visibility controls instead of user-specific folders
|
||||
|
||||
**Response:** HTTP 410 Gone with deprecation message
|
||||
|
||||
**Status Codes:**
|
||||
- 410: Gone (endpoint deprecated)
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
|
||||
**Replacement Endpoints:**
|
||||
- `GET /api/libraries` - List all libraries (admin)
|
||||
- `GET /api/libraries/visible` - List user's visible libraries
|
||||
- `POST /api/libraries` - Create new library
|
||||
- `POST /api/libraries/{id}/folders` - Add folder to library
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
meta {
|
||||
name: Create Ebook with Various ISBN Formats
|
||||
type: http
|
||||
seq: 9
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/ebooks
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "Ebook with ISBN-13 (with hyphens)",
|
||||
"author": "Test Author",
|
||||
"isbn": "978-0-12345-678-9",
|
||||
"description": "Testing ISBN normalization with hyphens",
|
||||
"file_path": "/path/to/ebook1.epub",
|
||||
"file_size": 1048576,
|
||||
"mime_type": "application/epub+zip"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Create Ebook with ISBN-13 (with hyphens)
|
||||
|
||||
**Expected Behavior:** ISBN should be normalized to `9780123456789`
|
||||
|
||||
This test demonstrates that ISBNs with hyphens are automatically normalized.
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
meta {
|
||||
name: Create Ebook with ISBN (with spaces)
|
||||
type: http
|
||||
seq: 10
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/ebooks
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "Ebook with ISBN-13 (with spaces)",
|
||||
"author": "Test Author",
|
||||
"isbn": "978 0123456789",
|
||||
"description": "Testing ISBN normalization with spaces",
|
||||
"file_path": "/path/to/ebook2.epub",
|
||||
"file_size": 1048576,
|
||||
"mime_type": "application/epub+zip"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Create Ebook with ISBN-13 (with spaces)
|
||||
|
||||
**Expected Behavior:** ISBN should be normalized to `9780123456789`
|
||||
|
||||
This test demonstrates that ISBNs with spaces are automatically normalized.
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
meta {
|
||||
name: Create Ebook - No Library Error
|
||||
type: http
|
||||
seq: 11
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/ebooks
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "Test Ebook",
|
||||
"isbn": "9780123456789",
|
||||
"description": "Testing error when no library exists",
|
||||
"file_path": "/path/to/ebook.epub",
|
||||
"file_size": 1048576,
|
||||
"mime_type": "application/epub+zip"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Create Ebook - No Library Error
|
||||
|
||||
**Expected Behavior:** Should return 400 Bad Request with error message: "no ebook library found. Please create an ebook library first"
|
||||
|
||||
This test demonstrates the graceful error handling when attempting to create an ebook without first creating an ebook library.
|
||||
|
||||
**Setup Required:**
|
||||
- Ensure NO ebook library exists in the database
|
||||
- Run as admin user
|
||||
|
||||
**Expected Response:**
|
||||
```json
|
||||
{
|
||||
"error": "no ebook library found. Please create an ebook library first"
|
||||
}
|
||||
```
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
meta {
|
||||
name: Create Ebook
|
||||
type: http
|
||||
seq: 8
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/ebooks
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "Sample Ebook Title",
|
||||
"author": "Author Name",
|
||||
"isbn": "9780123456789",
|
||||
"description": "A sample ebook description",
|
||||
"file_path": "/path/to/ebook.epub",
|
||||
"file_size": 1048576,
|
||||
"mime_type": "application/epub+zip",
|
||||
"cover_image_path": "/path/to/cover.jpg",
|
||||
"series": "Sample Series",
|
||||
"series_number": 1,
|
||||
"tags": "fiction,adventure",
|
||||
"asin": "B00EXAMPLE",
|
||||
"date_published": "2023-01-15",
|
||||
"publisher": "Sample Publisher",
|
||||
"contributors": "Contributor Name"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Create Ebook
|
||||
|
||||
Creates a new ebook entry in the database.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/ebooks
|
||||
|
||||
**Authentication:** Required (Admin only)
|
||||
|
||||
**Request Body:** JSON object with:
|
||||
- `title` (string, required): Ebook title (1-500 characters)
|
||||
- `author` (string, optional): Author name
|
||||
- `isbn` (string, optional): ISBN number (will be normalized to remove hyphens and spaces)
|
||||
- `description` (string, optional): Ebook description
|
||||
- `file_path` (string, required): Path to ebook file
|
||||
- `file_size` (integer, required): File size in bytes
|
||||
- `mime_type` (string, required): MIME type of ebook
|
||||
- `cover_image_path` (string, optional): Path to cover image
|
||||
- `series` (string, optional): Series name
|
||||
- `series_number` (integer, optional): Number in series
|
||||
- `tags` (string, optional): Comma-separated tags
|
||||
- `asin` (string, optional): Amazon ASIN
|
||||
- `date_published` (string, optional): Publication date (YYYY-MM-DD)
|
||||
- `publisher` (string, optional): Publisher name
|
||||
- `contributors` (string, optional): Contributors list
|
||||
|
||||
**Response:** Complete ebook object with all fields
|
||||
|
||||
**Status Codes:**
|
||||
- 201: Ebook created successfully
|
||||
- 400: Bad request (invalid data, or no ebook library exists)
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
- 500: Internal server error
|
||||
|
||||
**Features:**
|
||||
- Admin-only endpoint for creating ebooks
|
||||
- Full validation of required fields
|
||||
- Supports all ebook metadata fields
|
||||
- ISBN normalization: automatically removes hyphens and spaces (e.g., "978-0-12345-678-9" becomes "9780123456789")
|
||||
- Requires an existing ebook library to create ebooks
|
||||
- Associates ebook with creating admin user
|
||||
|
||||
**ISBN Formats Supported:**
|
||||
- ISBN-13: `9780123456789` or `978-0-12345-678-9` or `978-0123456789`
|
||||
- ISBN-10: `0123456789` or `0-12345-678-9`
|
||||
- All formats are automatically normalized (hyphens and spaces removed)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
meta {
|
||||
name: Create/Update Ebook Rating
|
||||
type: http
|
||||
seq: 7
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/ebooks/{{ebook_id}}/rating
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"rating": 7
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Create/Update Ebook Rating
|
||||
|
||||
Creates or updates the authenticated user's rating for a specific ebook.
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Ebook 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:**
|
||||
- `id` (string): Rating UUID
|
||||
- `ebook_id` (string): Ebook UUID
|
||||
- `user_id` (string): User UUID
|
||||
- `rating` (number): Rating value (1-10)
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Error Responses:**
|
||||
- 400: Invalid rating value (must be 1-10)
|
||||
- 401: Invalid authentication
|
||||
- 404: Ebook not found
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
meta {
|
||||
name: Delete Ebook Rating
|
||||
type: http
|
||||
seq: 8
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{base_url}}/api/ebooks/{{ebook_id}}/rating
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Delete Ebook Rating
|
||||
|
||||
Deletes the authenticated user's rating for a specific ebook.
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Ebook UUID
|
||||
|
||||
**Response:** No content (204)
|
||||
|
||||
**Error Responses:**
|
||||
- 401: Invalid authentication
|
||||
- 404: Rating not found
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
meta {
|
||||
name: Delete Ebook
|
||||
type: http
|
||||
seq: 10
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{base_url}}/api/ebooks/{{ebook_id}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Delete Ebook
|
||||
|
||||
Deletes an ebook from the database.
|
||||
|
||||
**Method:** DELETE
|
||||
|
||||
**Endpoint:** /api/ebooks/:id
|
||||
|
||||
**Authentication:** Required (Admin only)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Ebook ID (UUID)
|
||||
|
||||
**Response:** No content (204) or success message
|
||||
|
||||
**Status Codes:**
|
||||
- 204: Ebook deleted successfully (no content)
|
||||
- 400: Bad request (invalid ID format)
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
- 404: Ebook not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Features:**
|
||||
- Admin-only endpoint for deleting ebooks
|
||||
- Permanent deletion (cannot be undone)
|
||||
- Cascades to delete related data (reading progress, ratings)
|
||||
- Requires valid UUID format for ebook ID
|
||||
|
||||
**Important Notes:**
|
||||
- This is a destructive operation
|
||||
- All user reading progress for this ebook will be lost
|
||||
- All user ratings for this ebook will be lost
|
||||
- Use with caution
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
meta {
|
||||
name: Get Ebook Rating
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/ebooks/{{ebook_id}}/rating
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Ebook Rating
|
||||
|
||||
Retrieves the authenticated user's rating for a specific ebook.
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Ebook UUID
|
||||
|
||||
**Response when rating exists:**
|
||||
- `id` (string): Rating UUID
|
||||
- `ebook_id` (string): Ebook UUID
|
||||
- `user_id` (string): User UUID
|
||||
- `rating` (number): Rating value (1-10, where odd numbers = half-stars)
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Response when no rating exists:**
|
||||
- `ebook_id` (string): Ebook UUID
|
||||
- `user_id` (string): User UUID
|
||||
- `rating` (number): Always 0 (indicates no rating)
|
||||
|
||||
**Error Responses:**
|
||||
- 401: Invalid authentication
|
||||
|
||||
**Rating System:**
|
||||
- Backend: 1-10 scale (odd numbers = half-stars: 1,3,5,7,9)
|
||||
- Frontend: 1-5 stars with half-star precision (0.5, 1.5, 2.5, 3.5, 4.5)
|
||||
- Example: Backend rating 3 = Frontend display 1.5 stars
|
||||
|
||||
**Note:** This endpoint returns HTTP 200 with rating 0 when no rating exists, instead of HTTP 404.
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
meta {
|
||||
name: Get Ebook Ratings
|
||||
type: http
|
||||
seq: 9
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/ebooks/{{ebook_id}}/ratings
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Ebook Ratings
|
||||
|
||||
Retrieves all ratings for a specific ebook, including usernames.
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Ebook UUID
|
||||
|
||||
**Response:** Array of rating objects
|
||||
- `id` (string): Rating UUID
|
||||
- `ebook_id` (string): Ebook UUID
|
||||
- `user_id` (string): User UUID
|
||||
- `rating` (number): Rating value (1-10, where odd numbers = half-stars)
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
- `username` (string): Username of the rating user
|
||||
|
||||
**Rating Conversion:**
|
||||
- Frontend displays: 1-5 stars with half-star precision
|
||||
- Backend stores: 1-10 (where 1,3,5,7,9 = half-stars)
|
||||
|
||||
**Error Responses:**
|
||||
- 401: Invalid authentication
|
||||
- 404: Ebook not found
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
meta {
|
||||
name: Get Ebook
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/ebooks/{{ebook_id}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Get Ebook
|
||||
|
||||
Retrieves details of a specific ebook.
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Ebook UUID
|
||||
|
||||
**Response:** Complete ebook object with all metadata
|
||||
|
||||
**Error Responses:**
|
||||
- 401: Invalid authentication
|
||||
- 404: Ebook not found
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
meta {
|
||||
name: List Ebooks
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/ebooks
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## List Ebooks
|
||||
|
||||
Retrieves a paginated list of ebooks.
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Query Parameters:**
|
||||
- `limit` (number, optional): Number of results (default: 20, max: 100)
|
||||
- `offset` (number, optional): Pagination offset (default: 0)
|
||||
|
||||
**Response:** Array of ebook objects with id, title, author, etc.
|
||||
|
||||
**Error Responses:**
|
||||
- 401: Invalid authentication
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
meta {
|
||||
name: Update Ebook
|
||||
type: http
|
||||
seq: 9
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{base_url}}/api/ebooks/{{ebook_id}}
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"title": "Updated Ebook Title",
|
||||
"author": "Updated Author Name",
|
||||
"isbn": "978-0123456789",
|
||||
"description": "Updated ebook description",
|
||||
"cover_image_path": "/path/to/updated_cover.jpg",
|
||||
"series": "Updated Series",
|
||||
"series_number": 2,
|
||||
"tags": "fiction,adventure,updated",
|
||||
"asin": "B00EXAMPLE2",
|
||||
"date_published": "2023-02-15",
|
||||
"publisher": "Updated Publisher",
|
||||
"contributors": "Updated Contributor Name"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Update Ebook
|
||||
|
||||
Updates an existing ebook's metadata.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/ebooks/:id
|
||||
|
||||
**Authentication:** Required (Admin only)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Ebook ID (UUID)
|
||||
|
||||
**Request Body:** JSON object with:
|
||||
- `title` (string, required): Updated ebook title (1-500 characters)
|
||||
- `author` (string, optional): Updated author name
|
||||
- `isbn` (string, optional): Updated ISBN number
|
||||
- `description` (string, optional): Updated ebook description
|
||||
- `cover_image_path` (string, optional): Updated path to cover image
|
||||
- `series` (string, optional): Updated series name
|
||||
- `series_number` (integer, optional): Updated number in series
|
||||
- `tags` (string, optional): Updated comma-separated tags
|
||||
- `asin` (string, optional): Updated Amazon ASIN
|
||||
- `date_published` (string, optional): Updated publication date (YYYY-MM-DD)
|
||||
- `publisher` (string, optional): Updated publisher name
|
||||
- `contributors` (string, optional): Updated contributors list
|
||||
|
||||
**Response:** Updated ebook object with all fields
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Ebook updated successfully
|
||||
- 400: Bad request (invalid data or ID)
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin access required)
|
||||
- 404: Ebook not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Features:**
|
||||
- Admin-only endpoint for updating ebook metadata
|
||||
- Updates only provided fields, preserves others
|
||||
- Full validation of all fields
|
||||
- Cannot update file_path, file_size, or mime_type (file properties)
|
||||
}
|
||||
@@ -6,10 +6,11 @@ vars {
|
||||
media_item_id: 550e8400-e29b-41d4-a716-446655440000
|
||||
highlight_id: 660f9501-f29b-51d4-b716-446655440001
|
||||
note_id: 7710a602-g29b-61d4-c716-446655440002
|
||||
library_id: 8821b703-h29b-71d4-d716-446655440003
|
||||
library_id: cc23c3a7-f8fb-451a-a78d-2a16df1b725a
|
||||
job_id: 550e8400-e29b-41d4-a716-446655440000
|
||||
rating: 5
|
||||
is_visible: true
|
||||
library_folder: /app/uploads
|
||||
}
|
||||
vars:secret [
|
||||
token,
|
||||
|
||||
@@ -6,8 +6,8 @@ meta {
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/libraries/{{library_id}}/folders
|
||||
auth: inherit
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
@@ -16,56 +16,7 @@ headers {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"folder_path": "/path/to/library/media"
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
test_add_library_folder_success(status, headers, body) {
|
||||
if (status !== 201) {
|
||||
throw new Error("Expected status 201, 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 folder response
|
||||
if (!data.id) {
|
||||
throw new Error("Folder response missing required field: id");
|
||||
}
|
||||
|
||||
if (!data.library_id) {
|
||||
throw new Error("Folder response missing required field: library_id");
|
||||
}
|
||||
|
||||
if (!data.folder_path) {
|
||||
throw new Error("Folder response missing required field: folder_path");
|
||||
}
|
||||
|
||||
// Validate data types
|
||||
if (typeof data.id !== "string") {
|
||||
throw new Error("Folder id must be a string");
|
||||
}
|
||||
|
||||
if (typeof data.folder_path !== "string") {
|
||||
throw new Error("Folder path must be a string");
|
||||
}
|
||||
|
||||
return true;
|
||||
"folder_path": {{library_folder}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,4 +62,4 @@ docs {
|
||||
- 404: Library not found
|
||||
- 409: Folder already exists for this library
|
||||
- 500: Internal server error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,20 @@ body:json {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -6,6 +6,7 @@ meta {
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/libraries/{{library_id}}/folders
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
@@ -13,52 +14,6 @@ headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_get_library_folders_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 (!Array.isArray(data)) {
|
||||
throw new Error("Expected response body to be an array");
|
||||
}
|
||||
|
||||
// Validate each folder in array
|
||||
data.forEach((folder, index) => {
|
||||
if (!folder || typeof folder !== "object") {
|
||||
throw new Error("Folder at index " + index + " is not an object");
|
||||
}
|
||||
|
||||
if (!folder.id) {
|
||||
throw new Error("Folder at index " + index + " missing required field: id");
|
||||
}
|
||||
|
||||
if (!folder.folder_path) {
|
||||
throw new Error("Folder at index " + index + " missing required field: folder_path");
|
||||
}
|
||||
|
||||
if (!folder.library_id) {
|
||||
throw new Error("Folder at index " + index + " missing required field: library_id");
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
libraryId: "8821b703-h29b-71d4-d716-446655440003"
|
||||
}
|
||||
@@ -97,4 +52,4 @@ docs {
|
||||
- 403: Forbidden (library access denied)
|
||||
- 404: Library not found
|
||||
- 500: Internal server error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ meta {
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/libraries/types
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
@@ -13,48 +14,6 @@ headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_get_library_types_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 (!Array.isArray(data)) {
|
||||
throw new Error("Expected response body to be an array");
|
||||
}
|
||||
|
||||
// Validate each library type
|
||||
data.forEach((type, index) => {
|
||||
if (!type || typeof type !== "object") {
|
||||
throw new Error("Library type at index " + index + " is not an object");
|
||||
}
|
||||
|
||||
if (!type.id) {
|
||||
throw new Error("Library type at index " + index + " missing required field: id");
|
||||
}
|
||||
|
||||
if (!type.name) {
|
||||
throw new Error("Library type at index " + index + " missing required field: name");
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
@@ -80,4 +39,4 @@ docs {
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ meta {
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/library/scan-settings
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
@@ -13,42 +14,6 @@ headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_get_scan_settings_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
|
||||
if (typeof data.scan_frequency_minutes !== "number" || data.scan_frequency_minutes < 1) {
|
||||
throw new Error("Invalid scan_frequency_minutes: " + data.scan_frequency_minutes);
|
||||
}
|
||||
|
||||
if (typeof data.auto_scan_enabled !== "boolean") {
|
||||
throw new Error("Invalid auto_scan_enabled: " + data.auto_scan_enabled);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
@@ -56,25 +21,25 @@ settings {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ meta {
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/libraries/visible
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
@@ -13,52 +14,6 @@ headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_get_visible_libraries_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 (!Array.isArray(data)) {
|
||||
throw new Error("Expected response body to be an array");
|
||||
}
|
||||
|
||||
// Validate each library in array (all should be visible)
|
||||
data.forEach((library, index) => {
|
||||
if (!library || typeof library !== "object") {
|
||||
throw new Error("Library at index " + index + " is not an object");
|
||||
}
|
||||
|
||||
if (!library.id) {
|
||||
throw new Error("Library at index " + index + " missing required field: id");
|
||||
}
|
||||
|
||||
if (!library.name) {
|
||||
throw new Error("Library at index " + index + " missing required field: name");
|
||||
}
|
||||
|
||||
if (library.is_visible !== true) {
|
||||
throw new Error("Library at index " + index + " is not marked as visible");
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
@@ -88,4 +43,4 @@ docs {
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ meta {
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/libraries/visibility
|
||||
auth: inherit
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
@@ -21,42 +21,6 @@ body:json {
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
test_set_visibility_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 response
|
||||
if (!data.library_id) {
|
||||
throw new Error("Response missing required field: library_id");
|
||||
}
|
||||
|
||||
if (typeof data.is_visible !== "boolean") {
|
||||
throw new Error("Response missing required field: is_visible or not boolean");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
libraryId: "8821b703-h29b-71d4-d716-446655440003",
|
||||
isVisible: true
|
||||
@@ -96,4 +60,4 @@ docs {
|
||||
- 403: Forbidden (admin access required)
|
||||
- 404: Library not found
|
||||
- 500: Internal server error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ meta {
|
||||
|
||||
put {
|
||||
url: {{base_url}}/api/library/scan-settings
|
||||
auth: inherit
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
@@ -21,38 +21,6 @@ body:json {
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
test_update_scan_settings_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 success message or updated settings
|
||||
if (!data.message && typeof data.scan_frequency_minutes !== "number") {
|
||||
throw new Error("Response missing expected fields");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
@@ -60,74 +28,24 @@ settings {
|
||||
|
||||
docs {
|
||||
## Update Scan Settings
|
||||
|
||||
Updates user's library scanning settings.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/library/scan-settings
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Request Body:**
|
||||
- `scan_frequency_minutes` (number, required): Minutes between automatic scans (1-1440)
|
||||
- `auto_scan_enabled` (boolean, required): Whether automatic scanning is enabled
|
||||
- `library_id` (string, optional): Specific library ID to update (if not provided, updates global)
|
||||
|
||||
**Response:** Updated scan settings object
|
||||
- `scan_frequency_minutes` (number): Updated frequency
|
||||
- `auto_scan_enabled` (boolean): Updated enabled status
|
||||
- `last_scan_at` (string, optional): Timestamp of last scan
|
||||
- `next_scan_at` (string, optional): Timestamp of next scheduled scan
|
||||
- `updated_at` (string): Timestamp of settings update
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid settings (frequency out of range)
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (access denied)
|
||||
- 404: Library not found (if library_id provided)
|
||||
- 500: Internal server error
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{base_url}}/api/library/scan-settings
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body {
|
||||
{
|
||||
"scan_frequency_minutes": 60,
|
||||
"auto_scan_enabled": true
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
## Update Scan Settings
|
||||
|
||||
|
||||
Updates the user's ebook 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ meta {
|
||||
|
||||
post {
|
||||
url: {{base_url}}/api/media-items/{{media_item_id}}/rating
|
||||
auth: inherit
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
@@ -20,46 +20,6 @@ body:json {
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
test_create_rating_success(status, headers, body) {
|
||||
if (status !== 201) {
|
||||
throw new Error("Expected status 201, 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 rating response
|
||||
if (!data.id) {
|
||||
throw new Error("Rating response missing required field: id");
|
||||
}
|
||||
|
||||
if (!data.media_item_id) {
|
||||
throw new Error("Rating response missing required field: media_item_id");
|
||||
}
|
||||
|
||||
if (typeof data.rating !== "number" || data.rating < 1 || data.rating > 10) {
|
||||
throw new Error("Invalid rating value: " + data.rating + " (must be 1-10)");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
mediaItemId: "9932c704-i29b-81d4-e716-446655440004"
|
||||
}
|
||||
@@ -91,7 +51,7 @@ docs {
|
||||
**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
|
||||
@@ -99,7 +59,7 @@ docs {
|
||||
- `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)
|
||||
|
||||
@@ -6,6 +6,7 @@ meta {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -13,69 +14,6 @@ headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_filter_media_items(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 {
|
||||
const parsed = JSON.parse(body);
|
||||
data = parsed.data;
|
||||
} catch (e) {
|
||||
throw new Error("Response body is not valid JSON: " + e.message);
|
||||
}
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error("Expected response data to be an array");
|
||||
}
|
||||
|
||||
// Verify filters are applied
|
||||
data.forEach((item, index) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
throw new Error("Media item at index " + index + " is not an object");
|
||||
}
|
||||
|
||||
if (!item.id) {
|
||||
throw new Error("Media item at index " + index + " missing required field: id");
|
||||
}
|
||||
|
||||
if (!item.title) {
|
||||
throw new Error("Media item at index " + index + " missing required field: title");
|
||||
}
|
||||
|
||||
if (!item.library_id) {
|
||||
throw new Error("Media item at index " + index + " missing required field: library_id");
|
||||
}
|
||||
|
||||
// Check genre filter if genre is specified
|
||||
if (item.genre && item.genre !== 'Fiction') {
|
||||
throw new Error("Item at index " + index + " has incorrect genre: " + item.genre);
|
||||
}
|
||||
|
||||
// Check language filter if language is specified
|
||||
if (item.language && item.language !== 'en') {
|
||||
throw new Error("Item at index " + index + " has incorrect language: " + item.language);
|
||||
}
|
||||
|
||||
// Check year range if specified
|
||||
if (item.copyright_year) {
|
||||
if (item.copyright_year < 2000 || item.copyright_year > 2024) {
|
||||
throw new Error("Item at index " + index + " has copyright_year outside range: " + item.copyright_year);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
genre: "Fiction"
|
||||
language: "en"
|
||||
@@ -90,13 +28,13 @@ settings {
|
||||
|
||||
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)
|
||||
@@ -109,22 +47,22 @@ docs {
|
||||
- `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)
|
||||
|
||||
@@ -6,6 +6,7 @@ meta {
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/media-items?library_id={{library_id}}&sort=title+ASC&limit=10&offset=0
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
@@ -13,64 +14,6 @@ headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
tests {
|
||||
test_list_media_items_sorted(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 {
|
||||
const parsed = JSON.parse(body);
|
||||
data = parsed.data;
|
||||
} catch (e) {
|
||||
throw new Error("Response body is not valid JSON: " + e.message);
|
||||
}
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error("Expected response data to be an array");
|
||||
}
|
||||
|
||||
// Verify items are sorted by title ascending
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const prevTitle = data[i - 1].title.toLowerCase();
|
||||
const currTitle = data[i].title.toLowerCase();
|
||||
if (prevTitle > currTitle) {
|
||||
throw new Error("Items not sorted by title ASC: " + prevTitle + " should come before " + currTitle);
|
||||
}
|
||||
}
|
||||
|
||||
data.forEach((item, index) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
throw new Error("Media item at index " + index + " is not an object");
|
||||
}
|
||||
|
||||
if (!item.id) {
|
||||
throw new Error("Media item at index " + index + " missing required field: id");
|
||||
}
|
||||
|
||||
if (!item.title) {
|
||||
throw new Error("Media item at index " + index + " missing required field: title");
|
||||
}
|
||||
|
||||
if (!item.library_id) {
|
||||
throw new Error("Media item at index " + index + " missing required field: library_id");
|
||||
}
|
||||
|
||||
if (!item.library_name) {
|
||||
throw new Error("Media item at index " + index + " missing required field: library_name");
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
sortBy: "title ASC"
|
||||
}
|
||||
@@ -82,13 +25,13 @@ settings {
|
||||
|
||||
docs {
|
||||
## List Media Items with Sorting
|
||||
|
||||
|
||||
**Method:** GET
|
||||
|
||||
|
||||
**Endpoint:** /api/media-items
|
||||
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
|
||||
**Query Parameters:**
|
||||
- `library_id` (string, required): UUID of the library
|
||||
- `sort` (string, optional): Sort field and direction
|
||||
@@ -111,20 +54,20 @@ docs {
|
||||
- `genre DESC` - Genre Z-A
|
||||
- `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 media items
|
||||
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Bad request (invalid parameters)
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
|
||||
|
||||
**Examples:**
|
||||
- Sort by title: `/api/media-items?library_id=xxx&sort=title+ASC`
|
||||
- Sort by author descending: `/api/media-items?library_id=xxx&sort=author+DESC`
|
||||
- Sort by page count: `/api/media-items?library_id=xxx&sort=page_count+ASC`
|
||||
|
||||
|
||||
**Sorting Behavior:**
|
||||
- All sorts are secondary-sorted by series_number then title for consistency
|
||||
- NULL values are sorted last for ascending, first for descending
|
||||
|
||||
@@ -5,7 +5,7 @@ meta {
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/api/ebooks/{{ebook_id}}/progress
|
||||
url: {{base_url}}/api/media-items/{{media_item_id}}/progress
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
@@ -18,15 +18,15 @@ settings {
|
||||
docs {
|
||||
## Get Reading Progress
|
||||
|
||||
Retrieves the authenticated user's reading progress for an ebook.
|
||||
Retrieves the authenticated user's reading progress for a media item.
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Ebook UUID
|
||||
- `id` (string): Media Item UUID
|
||||
|
||||
**Response:**
|
||||
- `ebook_id` (string): Ebook UUID
|
||||
- `id` (string): Media Item UUID
|
||||
- `user_id` (string): User UUID
|
||||
- `current_page` (number): Current page number
|
||||
- `total_pages` (number, nullable): Total pages
|
||||
|
||||
@@ -5,7 +5,7 @@ meta {
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{base_url}}/api/ebooks/{{ebook_id}}/progress
|
||||
url: {{base_url}}/api/media-items/{{media_item_id}}/progress
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
@@ -25,12 +25,12 @@ settings {
|
||||
docs {
|
||||
## Update Reading Progress
|
||||
|
||||
Updates the authenticated user's reading progress for an ebook.
|
||||
Updates the authenticated user's reading progress for a media item.
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Ebook UUID
|
||||
- `id` (string): Media Item UUID
|
||||
|
||||
**Request Body:**
|
||||
- `current_page` (number, required): Current page number
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
meta {
|
||||
name: Scan Ebooks (Background)
|
||||
name: Scan Media Items (Background)
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
@@ -22,9 +22,9 @@ body:json {
|
||||
}
|
||||
|
||||
docs {
|
||||
## Scan Ebooks (Background)
|
||||
## Scan Media Items (Background)
|
||||
|
||||
Triggers an asynchronous ebook scanning operation. The scan runs in the background and can be monitored using the job ID.
|
||||
Triggers an asynchronous media items scanning operation. The scan runs in the background and can be monitored using the job ID.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
@@ -34,7 +34,7 @@ docs {
|
||||
|
||||
**Request Body:**
|
||||
- `folder_paths` (array of strings, required): List of folder paths to scan
|
||||
- Example: `["/path/to/ebooks", "/another/path"]`
|
||||
- Example: `["/path/to/media", "/another/path"]`
|
||||
|
||||
**Response:** HTTP 202 (Accepted)
|
||||
```json
|
||||
@@ -19,7 +19,7 @@ body:json {
|
||||
docs {
|
||||
## Start Watch Mode
|
||||
|
||||
Starts real-time file system monitoring for a specific library. New ebooks will be detected and processed almost instantly.
|
||||
Starts real-time file system monitoring for a specific library. New media items will be detected and processed almost instantly.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
|
||||
@@ -49,5 +49,5 @@ docs {
|
||||
- Cannot delete the last admin account in the system
|
||||
- Admin role required to use user_id parameter
|
||||
|
||||
**Warning:** This action cannot be undone and will permanently delete all user data including ebooks, ratings, and progress.
|
||||
**Warning:** This action cannot be undone and will permanently delete all user data including media items, ratings, and progress.
|
||||
}
|
||||
@@ -23,11 +23,11 @@ body:json {
|
||||
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.token) {
|
||||
return bru.setEnvVar("token", data.token, { persist: true });
|
||||
if (data && data.access_token) {
|
||||
|
||||
return bru.setEnvVar("token", data.access_token, { persist: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user