diff --git a/bruno/collections/add_auto_assign_rule.yml b/bruno/collections/add_auto_assign_rule.yml deleted file mode 100644 index 456c025d..00000000 --- a/bruno/collections/add_auto_assign_rule.yml +++ /dev/null @@ -1,41 +0,0 @@ -info: - name: Add Auto-Assign Rule - type: http - seq: 1 - -http: - method: POST - url: '{{base_url}}/api/collections/{{collection_id}}/rules' - auth: inherit - body: - type: json - data: |- - { - "field": "genre", - "operator": "equals", - "value": "Science Fiction", - "priority": 1, - "enabled": true - } -settings: - encodeUrl: true - timeout: 0 - followRedirects: true - maxRedirects: 5 - -runtime: - scripts: - - type: tests - code: |- - test("status is 201", function() { - expect(res.status).to.eql(201); - }); - -docs: - content: |- - # Add Auto-Assign Rule - - `POST /api/collections/:id/rules` — auth: jwt. - - Canonical reference: docs/developer/api/collections/add_auto_assign_rule.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update. - type: text/markdown diff --git a/bruno/collections/remove_auto_assign_rule.yml b/bruno/collections/remove_auto_assign_rule.yml deleted file mode 100644 index cdf0a866..00000000 --- a/bruno/collections/remove_auto_assign_rule.yml +++ /dev/null @@ -1,31 +0,0 @@ -info: - name: Remove Auto-Assign Rule - type: http - seq: 13 - -http: - method: DELETE - url: '{{base_url}}/api/collections/{{collection_id}}/rules/{{rule_id}}' - auth: inherit -settings: - encodeUrl: true - timeout: 0 - followRedirects: true - maxRedirects: 5 - -runtime: - scripts: - - type: tests - code: |- - test("status is 204", function() { - expect(res.status).to.eql(204); - }); - -docs: - content: |- - # Remove Auto-Assign Rule - - `DELETE /api/collections/:id/rules/:ruleId` — auth: jwt. - - Canonical reference: docs/developer/api/collections/remove_auto_assign_rule.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update. - type: text/markdown diff --git a/bruno/collections/set_chain_order.yml b/bruno/collections/set_chain_order.yml new file mode 100644 index 00000000..5b2fc6d1 --- /dev/null +++ b/bruno/collections/set_chain_order.yml @@ -0,0 +1,29 @@ +info: + name: Set Collection Chain Order + type: http + seq: 13 + +http: + method: PUT + url: '{{base_url}}/api/collections/{{collection_id}}/view-settings' + auth: inherit + body: + type: json + data: |- + { + "chain_order": "newest" + } +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 + +docs: + content: |- + # Set Collection Chain Order + + `PUT /api/collections/:id/view-settings` — auth: jwt. + + Canonical reference: docs/developer/api/collections/set_chain_order.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update. + type: text/markdown diff --git a/bruno/collections/test_rule.yml b/bruno/collections/test_rule.yml deleted file mode 100644 index f3f0caf2..00000000 --- a/bruno/collections/test_rule.yml +++ /dev/null @@ -1,48 +0,0 @@ -info: - name: Test Rule - type: http - seq: 15 - -http: - method: POST - url: '{{base_url}}/api/collections/test-rules' - auth: inherit - body: - type: json - data: |- - { - "rules": [ - { - "field": "genre", - "operator": "equals", - "value": "Science Fiction" - }, - { - "field": "author", - "operator": "contains", - "value": "Asimov" - } - ] - } -settings: - encodeUrl: true - timeout: 0 - followRedirects: true - maxRedirects: 5 - -runtime: - scripts: - - type: tests - code: |- - test("status is 200", function() { - expect(res.status).to.eql(200); - }); - -docs: - content: |- - # Test Rule - - `POST /api/collections/test-rules` — auth: jwt. - - Canonical reference: docs/developer/api/collections/test_rule.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update. - type: text/markdown diff --git a/bruno/collections/update_rule.yml b/bruno/collections/update_rule.yml deleted file mode 100644 index 780a709f..00000000 --- a/bruno/collections/update_rule.yml +++ /dev/null @@ -1,29 +0,0 @@ -info: - name: Update Collection Rule - type: http - seq: 17 - -http: - method: PUT - url: '{{base_url}}/api/collections/{{collection_id}}/rules/{{rule_id}}' - auth: inherit - body: - type: json - data: |- - { - "enabled": false - } -settings: - encodeUrl: true - timeout: 0 - followRedirects: true - maxRedirects: 5 - -docs: - content: |- - # Update Collection Rule - - `PUT /api/collections/:id/rules/:ruleId` — auth: jwt. - - Canonical reference: docs/developer/api/collections/update_rule.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update. - type: text/markdown diff --git a/docs/developer/api/collections/add_auto_assign_rule.md b/docs/developer/api/collections/add_auto_assign_rule.md deleted file mode 100644 index 5ed8176a..00000000 --- a/docs/developer/api/collections/add_auto_assign_rule.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -method: POST -path: /api/collections/:id/rules -auth: jwt -area: collections ---- - - -# Add Auto-Assign Rule - -Add an automatic book assignment rule to a collection. - -**Endpoint**: `POST /api/collections/{id}/rules` -**Auth**: Required -**Content-Type**: `application/json` - -## Path Parameters - -| Parameter | Type | Required | Description | -| --------- | ------------- | -------- | --------------- | -| id | string (UUID) | Yes | Collection UUID | - -## Request Body - -| Field | Type | Required | Description | -| -------- | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------- | -| field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) | -| operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) | -| value | string/number | Yes | Value to compare against | -| priority | integer | No | Rule priority (1 = highest, default: 1) | -| enabled | boolean | No | Whether rule is active (default: true) | - -### Supported Fields - -| Field | Type | Description | -| -------------- | ------ | ------------------------------------- | -| genre | string | Book genre | -| author | string | Book author | -| series | string | Book series name | -| language | string | Book language | -| publisher | string | Publisher name | -| copyright_year | number | Publication year (numeric comparison) | -| tags | string | Book tags | - -### Supported Operators - -| Operator | Type | Description | -| ------------ | ------ | ------------------------------------- | -| equals | all | Exact match | -| not_equals | all | Not equal | -| contains | string | Contains substring (case-insensitive) | -| not_contains | string | Does not contain | -| starts_with | string | Starts with (case-insensitive) | -| ends_with | string | Ends with (case-insensitive) | -| greater_than | number | Greater than | -| less_than | number | Less than | - -### Example Request - -```json example-request -{ - "field": "genre", - "operator": "equals", - "value": "Science Fiction", - "priority": 1, - "enabled": true -} -``` - -## Response (201 Created) - -```json example-response -{ - "collection_id": null, - "is_smart": null, - "rule": null -} -``` - -## Error Responses - -| Code | Description | -| ---- | ----------------------------------- | -| 400 | Invalid request (validation failed) | -| 401 | Authentication required | -| 404 | Collection not found | -| 500 | Internal server error | - -## Try It Out diff --git a/docs/developer/api/collections/remove_auto_assign_rule.md b/docs/developer/api/collections/remove_auto_assign_rule.md deleted file mode 100644 index f9b89887..00000000 --- a/docs/developer/api/collections/remove_auto_assign_rule.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -method: DELETE -path: /api/collections/:id/rules/:ruleId -auth: jwt -area: collections ---- - - -# Remove Auto-Assign Rule - -Remove an automatic book assignment rule from a collection. - -**Endpoint**: `DELETE /api/collections/{collectionId}/rules/{ruleId}` -**Auth**: Required -**Content-Type**: `application/json` - -## Path Parameters - -| Parameter | Type | Required | Description | -| ------------ | ------------- | -------- | --------------- | -| collectionId | string (UUID) | Yes | Collection UUID | -| ruleId | string (UUID) | Yes | Rule UUID | - -## Response (204 No Content) - -Rule deleted successfully. No response body. - -```json example-response -{ - "collection_id": "550e8400-e29b-41d4-a716-446655440000", - "is_smart": true -} -``` - -`is_smart` reflects the collection state AFTER removal — removing the -last rule converts a smart collection back to static. - -## Error Responses - -| Code | Description | -| ---- | ---------------------------- | -| 401 | Authentication required | -| 404 | Collection or rule not found | -| 500 | Internal server error | - -## Try It Out diff --git a/docs/developer/api/collections/set_chain_order.md b/docs/developer/api/collections/set_chain_order.md new file mode 100644 index 00000000..f9c4a0c3 --- /dev/null +++ b/docs/developer/api/collections/set_chain_order.md @@ -0,0 +1,48 @@ +--- +method: PUT +path: /api/collections/:id/view-settings +auth: jwt +area: collections +--- + + +# Set Collection Chain Order + +Sets the curated listening order for the collection's episode sequence +(plan §10 R2): the chain producer (`buildSmartCollectionSequenceContext`) +plays `Play as Sequence` in this order. The key merges into the +collection's `view_settings` JSONB — playlist toggles that share the +document survive the write. Owner-only. + +## Example request + +```json example-request +{ + "chain_order": "newest" +} +``` + +## Example response + +```json example-response +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "chain_order": "newest", + "view_settings": { "chain_order": "newest" } +} +``` + +## Fields + +| Field | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| chain_order | string | Yes | One of `native`, `newest`, `oldest`, `reverse`; `native`/empty removes the stored key | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "invalid collection id"}` | malformed id | +| 400 | `{"error": "chain_order must be one of: native, newest, oldest, reverse"}` | unknown order | +| 404 | `{"error": "collection not found"}` | unknown id | +| 401 / 403 | — | missing JWT / not the owner | diff --git a/docs/developer/api/collections/test_rule.md b/docs/developer/api/collections/test_rule.md deleted file mode 100644 index 9b55e02f..00000000 --- a/docs/developer/api/collections/test_rule.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -method: POST -path: /api/collections/test-rules -auth: jwt -area: collections ---- - - -# Test Rule - -Test which books would match given rules without saving. - -**Endpoint**: `POST /api/collections/test-rules` -**Auth**: Required -**Content-Type**: `application/json` - -## Request Body - -| Field | Type | Required | Description | -| ----- | ----- | -------- | ----------------------------- | -| rules | array | Yes | Array of rule objects to test | - -### Rule Object - -| Field | Type | Required | Description | -| -------- | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------- | -| field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) | -| operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) | -| value | string/number | Yes | Value to compare against | - -### Example Request - -```json example-request -{ - "rules": [ - { - "field": "genre", - "operator": "equals", - "value": "Science Fiction" - }, - { - "field": "author", - "operator": "contains", - "value": "Asimov" - } - ] -} -``` - -## Response (200 OK) - -```json example-response -{ - "matches": [ - { - "media_item_id": "660e8400-e29b-41d4-a716-446655440000", - "title": "Foundation", - "author": "Isaac Asimov", - "cover_image_path": "/covers/foundation.jpg", - "match_reason": "Matched rule: genre equals Science Fiction" - } - ], - "total": 42 -} -``` - -## Use Case - -Test rules before creating a collection to verify correct book matching. This endpoint shows which books would be added without actually modifying any collections. - -## Error Responses - -| Code | Description | -| ---- | ----------------------------------- | -| 400 | Invalid request (validation failed) | -| 401 | Authentication required | -| 500 | Internal server error | - -## Try It Out diff --git a/docs/developer/api/collections/update_rule.md b/docs/developer/api/collections/update_rule.md deleted file mode 100644 index 2f31f937..00000000 --- a/docs/developer/api/collections/update_rule.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -method: PUT -path: /api/collections/:id/rules/:ruleId -auth: jwt -area: collections ---- - - -# Update Collection Rule - -Despite the route name, this endpoint **toggles a rule's enabled state only**. -It does not update the rule's field/operator/value — rules are replaced wholesale -via [Create Collection](create_collection.md) (`auto_assign_rules`) or added via -the rules endpoint. - -## Example request - -```json example-request -{ - "enabled": false -} -``` - -## Example response - -```json example-response -{ - "collection_id": "550e8400-e29b-41d4-a716-446655440000", - "is_smart": true -} -``` - -## Request fields - -| Field | Type | Required | Notes | -| ----- | ---- | -------- | ----- | -| enabled | boolean | yes | toggles the rule on/off | - -## Errors - -| Status | Body | When | -| ------ | ---- | ---- | -| 400 | `{"error": "invalid collection id"}` / `{"error": "rule id required"}` / `{"error": "invalid request"}` | malformed | -| 404 | `{"error": "rule not found"}` | no such rule | -| 500 | `{"error": "..."}` | unexpected server error | diff --git a/docs/developer/api/libraries/add_library_folder.md b/docs/developer/api/libraries/add_library_folder.md index ad51043c..d82db107 100644 --- a/docs/developer/api/libraries/add_library_folder.md +++ b/docs/developer/api/libraries/add_library_folder.md @@ -41,7 +41,8 @@ Add a folder to an existing library (Admin only). "id": "uuid", "library_id": "uuid", "folder_path": "/path/to/comics", - "created_at": "2026-01-31T10:00:00Z" + "created_at": "2026-01-31T10:00:00Z", + "purpose": "scan" } ``` diff --git a/docs/developer/api/libraries/create_library.md b/docs/developer/api/libraries/create_library.md index 84800146..49f22968 100644 --- a/docs/developer/api/libraries/create_library.md +++ b/docs/developer/api/libraries/create_library.md @@ -42,7 +42,8 @@ Create a new library (Admin only). "created_at": "2026-01-31T10:00:00Z", "created_by_admin_id": null, "library_type_id": null, - "updated_at": null + "updated_at": null, + "allow_member_feeds": false } ``` diff --git a/docs/developer/api/libraries/get_library.md b/docs/developer/api/libraries/get_library.md index 79ab1035..e508b84e 100644 --- a/docs/developer/api/libraries/get_library.md +++ b/docs/developer/api/libraries/get_library.md @@ -43,7 +43,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... "created_at": null, "created_by_admin_id": null, "library_type_id": null, - "updated_at": null + "updated_at": null, + "allow_member_feeds": false } ``` diff --git a/docs/developer/api/libraries/get_library_folders.md b/docs/developer/api/libraries/get_library_folders.md index 17e8191e..14e0517b 100644 --- a/docs/developer/api/libraries/get_library_folders.md +++ b/docs/developer/api/libraries/get_library_folders.md @@ -18,7 +18,8 @@ Lists the filesystem folders configured for a library. "id": "aa0e8400-e29b-41d4-a716-446655440000", "library_id": "990e8400-e29b-41d4-a716-446655440000", "folder_path": "/books/manga", - "created_at": "2026-01-01T00:00:00Z" + "created_at": "2026-01-01T00:00:00Z", + "purpose": "scan" } ] ``` diff --git a/docs/developer/api/libraries/get_visible_libraries.md b/docs/developer/api/libraries/get_visible_libraries.md index dd91a76f..0df725f3 100644 --- a/docs/developer/api/libraries/get_visible_libraries.md +++ b/docs/developer/api/libraries/get_visible_libraries.md @@ -42,7 +42,8 @@ A top-level JSON **array** of library rows: "updated_at": "2026-01-31T10:00:00Z", "type_name": "ebooks", "type_description": "Ebook libraries", - "is_visible": true + "is_visible": true, + "allow_member_feeds": false } ] ``` diff --git a/docs/developer/api/libraries/update_library.md b/docs/developer/api/libraries/update_library.md index 4060dfc4..aa9985ff 100644 --- a/docs/developer/api/libraries/update_library.md +++ b/docs/developer/api/libraries/update_library.md @@ -46,7 +46,8 @@ Update a library's information. "created_at": "2026-02-08T10:00:00Z", "updated_at": "2026-02-08T11:00:00Z", "created_by_admin_id": null, - "description": null + "description": null, + "allow_member_feeds": false } ``` diff --git a/docs/developer/api/media-items/create_media_item.md b/docs/developer/api/media-items/create_media_item.md index a89f78f1..272da171 100644 --- a/docs/developer/api/media-items/create_media_item.md +++ b/docs/developer/api/media-items/create_media_item.md @@ -164,7 +164,23 @@ Tags and contributors are automatically normalized: "summary": null, "total_characters": null, "volume": null, - "web_url": null + "web_url": null, + "source": null, + "source_feed_id": null, + "feed_position": null, + "episode_number": null, + "season_number": null, + "episode_guid": null, + "enclosure_url": null, + "chapters_url": null, + "transcript_url": null, + "hosts": null, + "guests": null, + "declared_duration_seconds": null, + "download_state": null, + "download_attempts": null, + "download_queue_pos": null, + "download_error": null } ``` diff --git a/docs/developer/api/media-items/search_media_items.md b/docs/developer/api/media-items/search_media_items.md index ae499aee..542be616 100644 --- a/docs/developer/api/media-items/search_media_items.md +++ b/docs/developer/api/media-items/search_media_items.md @@ -231,7 +231,23 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... "total_characters": null, "updated_at": null, "volume": null, - "web_url": null + "web_url": null, + "source": null, + "source_feed_id": null, + "feed_position": null, + "episode_number": null, + "season_number": null, + "episode_guid": null, + "enclosure_url": null, + "chapters_url": null, + "transcript_url": null, + "hosts": null, + "guests": null, + "declared_duration_seconds": null, + "download_state": null, + "download_attempts": null, + "download_queue_pos": null, + "download_error": null } ] ``` diff --git a/docs/developer/api/media-items/update_media_item.md b/docs/developer/api/media-items/update_media_item.md index cb46d6a2..0fcfd71e 100644 --- a/docs/developer/api/media-items/update_media_item.md +++ b/docs/developer/api/media-items/update_media_item.md @@ -128,7 +128,23 @@ Update media item metadata (Admin only). "tags_search": null, "total_characters": null, "volume": null, - "web_url": null + "web_url": null, + "source": null, + "source_feed_id": null, + "feed_position": null, + "episode_number": null, + "season_number": null, + "episode_guid": null, + "enclosure_url": null, + "chapters_url": null, + "transcript_url": null, + "hosts": null, + "guests": null, + "declared_duration_seconds": null, + "download_state": null, + "download_attempts": null, + "download_queue_pos": null, + "download_error": null } ``` diff --git a/docs/developer/openapi.yaml b/docs/developer/openapi.yaml index 738d0d6b..b5884e94 100644 --- a/docs/developer/openapi.yaml +++ b/docs/developer/openapi.yaml @@ -277,33 +277,6 @@ paths: responses: '200': description: Success - "/api/collections/test-rules": - post: - tags: [collections] - summary: "Test Rule" - security: - - bearerAuth: [] - requestBody: - content: - application/json: - example: |- - { - "rules": [ - { - "field": "genre", - "operator": "equals", - "value": "Science Fiction" - }, - { - "field": "author", - "operator": "contains", - "value": "Asimov" - } - ] - } - responses: - '200': - description: OK "/api/collections/{id}": delete: tags: [collections] @@ -409,38 +382,10 @@ paths: responses: '200': description: Success - "/api/collections/{id}/rules": - post: - tags: [collections] - summary: "Add Auto-Assign Rule" - security: - - bearerAuth: [] - requestBody: - content: - application/json: - example: |- - { - "field": "genre", - "operator": "equals", - "value": "Science Fiction", - "priority": 1, - "enabled": true - } - responses: - '201': - description: Created - "/api/collections/{id}/rules/{ruleId}": - delete: - tags: [collections] - summary: "Remove Auto-Assign Rule" - security: - - bearerAuth: [] - responses: - '204': - description: No Content + "/api/collections/{id}/view-settings": put: tags: [collections] - summary: "Update Collection Rule" + summary: "Set Collection Chain Order" security: - bearerAuth: [] requestBody: @@ -448,11 +393,11 @@ paths: application/json: example: |- { - "enabled": false + "chain_order": "newest" } responses: '200': - description: Success + description: OK "/api/conflicts": get: tags: [conflicts] diff --git a/internal/docs/schema.go b/internal/docs/schema.go index 73a2f2e8..16104694 100644 --- a/internal/docs/schema.go +++ b/internal/docs/schema.go @@ -601,6 +601,14 @@ var Schemas = map[string]Schema{ Request: json.RawMessage(`{"item_ids": []}`), Response: json.RawMessage(`{"status": "reordered"}`), }, + "PUT /api/collections/:id/view-settings": { + // Merges chain_order into the collection's view_settings JSONB + // (playlist toggles share the doc and are preserved). + Request: json.RawMessage(`{"chain_order": "newest"}`), + Response: json.RawMessage(`{ + "id": "", "chain_order": "", "view_settings": {} + }`), + }, "PUT /api/collections/:id/rules/:ruleId": { // Handler is SetRuleEnabled: toggle only, request {enabled required}, // response {collection_id, is_smart}.