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:
2026-01-30 13:52:06 -05:00
parent dc820dfb92
commit ba31e1491e
33 changed files with 81 additions and 1290 deletions
-50
View File
@@ -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)
}
-54
View File
@@ -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
}
-44
View File
@@ -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"
}
```
}
-87
View File
@@ -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
}
-33
View File
@@ -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
}
-53
View File
@@ -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
}
-50
View File
@@ -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.
}
-44
View File
@@ -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
}
-33
View File
@@ -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
}
-33
View File
@@ -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
}
-78
View File
@@ -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)
}
+2 -1
View File
@@ -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,
+3 -52
View File
@@ -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
}
}
+14
View File
@@ -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
+2 -47
View File
@@ -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
}
}
+2 -43
View File
@@ -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
}
}
+8 -43
View File
@@ -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
}
}
+2 -47
View File
@@ -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
}
}
+2 -38
View File
@@ -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
}
}
+9 -91
View File
@@ -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
}
}
+3 -43
View File
@@ -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)
+9 -71
View File
@@ -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)
+9 -66
View File
@@ -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
+4 -4
View File
@@ -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
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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.
}
+3 -3
View File
@@ -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 });
}
}
}