From d3cd5f34f9df5841cfb81dc91e1352119dce66fb Mon Sep 17 00:00:00 2001 From: Yuriy Date: Fri, 8 May 2026 13:36:28 +0200 Subject: [PATCH 01/14] Add collections Add the concept of a collection with basic CRUD functionality. A collection is an ordered list of items - songs, artist, albums, playlists (other types can be added in the future). Users can create, modify and delete collections. The proposed endpoints and schemas closely follow those for playlists, however, items don't have to be just songs. Additionally, the `updateCollection` endpoint has a more elaborate mechanism for adding, removing and moving items around via the `add`, `remove`, and `move` parameters respectively. The request payloads - where applicable - should be formatted as JSON. --- content/en/docs/Endpoints/createcollection.md | 133 +++++++++ content/en/docs/Endpoints/deletecollection.md | 43 +++ content/en/docs/Endpoints/getcollection.md | 108 +++++++ content/en/docs/Endpoints/getcollections.md | 71 +++++ content/en/docs/Endpoints/updatecollection.md | 91 ++++++ content/en/docs/Extensions/collections.md | 25 ++ content/en/docs/Payloads/CollectionItemID.md | 31 ++ content/en/docs/Responses/collection.md | 47 +++ content/en/docs/Responses/collectionItem.md | 268 ++++++++++++++++++ .../en/docs/Responses/collectionWithItems.md | 97 +++++++ openapi/endpoints/createCollection.json | 68 +++++ .../CreateCollectionResponse.json | 20 ++ .../CreateCollectionSuccessResponse.json | 18 ++ openapi/endpoints/deleteCollection.json | 34 +++ openapi/endpoints/getCollection.json | 41 +++ .../getCollection/GetCollectionResponse.json | 20 ++ .../GetCollectionSuccessResponse.json | 18 ++ openapi/endpoints/getCollections.json | 41 +++ .../GetCollectionsResponse.json | 20 ++ .../GetCollectionsSuccessResponse.json | 22 ++ openapi/endpoints/updateCollection.json | 82 ++++++ openapi/openapi.json | 48 ++++ openapi/schemas/Collection.json | 67 +++++ openapi/schemas/CollectionItem.json | 38 +++ openapi/schemas/CollectionItemId.json | 28 ++ openapi/schemas/CollectionWithItems.json | 26 ++ openapi/schemas/Collections.json | 17 ++ 27 files changed, 1522 insertions(+) create mode 100644 content/en/docs/Endpoints/createcollection.md create mode 100644 content/en/docs/Endpoints/deletecollection.md create mode 100644 content/en/docs/Endpoints/getcollection.md create mode 100644 content/en/docs/Endpoints/getcollections.md create mode 100644 content/en/docs/Endpoints/updatecollection.md create mode 100644 content/en/docs/Extensions/collections.md create mode 100644 content/en/docs/Payloads/CollectionItemID.md create mode 100644 content/en/docs/Responses/collection.md create mode 100644 content/en/docs/Responses/collectionItem.md create mode 100644 content/en/docs/Responses/collectionWithItems.md create mode 100644 openapi/endpoints/createCollection.json create mode 100644 openapi/endpoints/createCollection/CreateCollectionResponse.json create mode 100644 openapi/endpoints/createCollection/CreateCollectionSuccessResponse.json create mode 100644 openapi/endpoints/deleteCollection.json create mode 100644 openapi/endpoints/getCollection.json create mode 100644 openapi/endpoints/getCollection/GetCollectionResponse.json create mode 100644 openapi/endpoints/getCollection/GetCollectionSuccessResponse.json create mode 100644 openapi/endpoints/getCollections.json create mode 100644 openapi/endpoints/getCollections/GetCollectionsResponse.json create mode 100644 openapi/endpoints/getCollections/GetCollectionsSuccessResponse.json create mode 100644 openapi/endpoints/updateCollection.json create mode 100644 openapi/schemas/Collection.json create mode 100644 openapi/schemas/CollectionItem.json create mode 100644 openapi/schemas/CollectionItemId.json create mode 100644 openapi/schemas/CollectionWithItems.json create mode 100644 openapi/schemas/Collections.json diff --git a/content/en/docs/Endpoints/createcollection.md b/content/en/docs/Endpoints/createcollection.md new file mode 100644 index 00000000..92b59e03 --- /dev/null +++ b/content/en/docs/Endpoints/createcollection.md @@ -0,0 +1,133 @@ +--- +title: "createCollection" +linkTitle: "createCollection [OS]" +categories: +- Collections +opensubsonic: +- Extension +description: > + Creates a collection. +--- + +`http://your-server/rest/createCollection` + +Creates a collection. +This endpoint must be accessed using an HTTP POST request. + +### Request Body + +The request payload should be provided in the body as a JSON object. + +| Parameter | Req. | OpenS. | Default | Comment | +| --- | --- | --- | --- | --- | +| `name` | **Yes** | | | The human-readable name of the collection. | +| `comment` | No | | | The collection comment. | +| `public` | No | | `false` | `true` if the collection should be visible to all users, `false` otherwise. | +| `items` | No | | | A list of [collectionItemID](../payloads/collectionitemid) objects. | + +### Example request + +{{< alert color="primary" >}} `POST http://your-server/rest/createCollection.view?u=demo&p=demo&v=1.16.1&c=AwesomeClientName&f=json` {{< /alert >}} + +{{< tabpane persist=false >}} +{{< tab header="Request body" lang="json">}} +{ + "name": "test collection", + "comment": "this is a collection", + "items": [ + { + "type": "song", + "id": "300000060" + }, + { + "type": "album", + "id": "200000021" + } + ] +} +{{< /tab >}} +{{< /tabpane >}} + +### Result + +A [`subsonic-response`](../../responses/subsonic-response) element with a top-level [`collection`](../../responses/collection) object on success. + +{{< tabpane persist=false >}} +{{< tab header="**Example**:" disabled=true />}} +{{< tab header="OpenSubsonic" lang="json">}} +{ + "subsonic-response": { + "status": "ok", + "version": "1.16.1", + "type": "AwesomeServerName", + "serverVersion": "0.1.3 (tag)", + "openSubsonic": true, + "collection": { + "id": "800000075", + "name": "test collection", + "comment" "this is a collection", + "owner": "user", + "public": false, + "itemCount": 2, + "created": "2023-03-16T03:18:41+00:00", + "changed": "2023-03-16T03:18:41+00:00", + "items": [ + { + "song": { + "id": "300000060", + "parent": "200000002", + "title": "BrownSmoke", + "isDir": false, + "isVideo": false, + "type": "music", + "albumId": "200000002", + "album": "Colorsmoke EP", + "artistId": "100000002", + "artist": "Synthetic", + "coverArt": "300000060", + "duration": 304, + "bitRate": 20, + "bitDepth": 16, + "samplingRate": 44100, + "channelCount": 2, + "userRating": 5, + "averageRating": 5, + "track": 4, + "year": 2007, + "genre": "Electronic", + "size": 792375, + "discNumber": 1, + "suffix": "wma", + "contentType": "audio/x-ms-wma", + "path": "Synthetic/Synthetic_-_Colorsmoke_EP-20k217-2007/04-Synthetic_-_BrownSmokeYSBM20k22khS.wma" + } + }, + { + "album": { + "id": "200000021", + "parent": "100000036", + "album": "Forget and Remember", + "title": "Forget and Remember", + "name": "Forget and Remember", + "isDir": true, + "coverArt": "al-200000021", + "songCount": 20, + "created": "2021-07-22T02:09:31+00:00", + "duration": 4248, + "playCount": 0, + "artistId": "100000036", + "artist": "Comfort Fit", + "year": 2005, + "genre": "Hip-Hop" + } + } + ] + } + } +} +{{< /tab >}} +{{< /tabpane >}} + +| Field | Type | Req. | OpenS. | Details | +| --- | --- | --- | --- | --- | +| `collection` | [`CollectionWithItems`](../../responses/collectionwithitems) | **Yes** | | The collection | diff --git a/content/en/docs/Endpoints/deletecollection.md b/content/en/docs/Endpoints/deletecollection.md new file mode 100644 index 00000000..2b92c074 --- /dev/null +++ b/content/en/docs/Endpoints/deletecollection.md @@ -0,0 +1,43 @@ +--- +title: "deleteCollection" +linkTitle: "deleteCollection [OS]" +opensubsonic: +- Extension +categories: +- Collections +description: > + Deletes a collection. +--- +`http://your-server/rest/deleteCollection` + +Deletes a collection. +This endpoint must be accessed using an HTTP DELETE request + +### Parameters + +| Parameter | Req. | OpenS. | Default | Comment | +| --- | --- | --- | --- | --- | +| `id` | **Yes** | | | ID of the collection to delete, as obtained by `getCollections`. | + +### Example + +{{< alert color="primary" >}} `DELETE http://your-server/rest/deleteCollection.view?id=123&u=demo&p=demo&v=1.13.0&c=AwesomeClientName&f=json` {{< /alert >}} + +### Result + +An empty [`subsonic-response`](../../responses/subsonic-response) element on success. + +{{< tabpane persist=false >}} +{{< tab header="**Example**:" disabled=true />}} +{{< tab header="OpenSubsonic" lang="json">}} +{ + "subsonic-response": { + "status": "ok", + "version": "1.16.1", + "type": "AwesomeServerName", + "serverVersion": "0.1.3 (tag)", + "openSubsonic": true + } +} +{{< /tab >}} +{{< /tabpane >}} diff --git a/content/en/docs/Endpoints/getcollection.md b/content/en/docs/Endpoints/getcollection.md new file mode 100644 index 00000000..ed54e73e --- /dev/null +++ b/content/en/docs/Endpoints/getcollection.md @@ -0,0 +1,108 @@ +--- +title: "getCollection" +linkTitle: "getCollection [OS]" +categories: +- Collections +OpenSubsonic: +- Extension +description: > + Returns a collection. +--- + +`http://your-server/rest/getCollection` + +Returns a collection with the items it contains. + +### Parameters + +| Parameter | Req. | OpenS. | Default | Comment | +| --- | --- | --- | --- | --- | +| `id` | **Yes** | | | ID of the collection to return, as obtained by `getCollections`. | + +### Example + +{{< alert color="primary" >}} `http://your-server/rest/getCollection.view?id=123&u=demo&p=demo&v=1.13.0&c=AwesomeClientName&f=json` {{< /alert >}} + +### Result + +A [`subsonic-response`](../../responses/subsonic-response) element with a top-level `collection` object of type [collectionWithItems](../../responses/collectionwithitems) + +{{< tabpane persist=false >}} +{{< tab header="**Example**:" disabled=true />}} +{{< tab header="OpenSubsonic" lang="json">}} +{ + "subsonic-response": { + "status": "ok", + "version": "1.16.1", + "type": "AwesomeServerName", + "serverVersion": "0.1.3 (tag)", + "openSubsonic": true, + "collection": { + "id": "800000075", + "name": "testcreate", + "comment" "this is a collection", + "owner": "user", + "public": true, + "itemCount": 2, + "created": "2023-03-16T03:18:41+00:00", + "changed": "2023-03-16T03:18:41+00:00", + "items": [ + { + "song": { + "id": "300000060", + "parent": "200000002", + "title": "BrownSmoke", + "isDir": false, + "isVideo": false, + "type": "music", + "albumId": "200000002", + "album": "Colorsmoke EP", + "artistId": "100000002", + "artist": "Synthetic", + "coverArt": "300000060", + "duration": 304, + "bitRate": 20, + "bitDepth": 16, + "samplingRate": 44100, + "channelCount": 2, + "userRating": 5, + "averageRating": 5, + "track": 4, + "year": 2007, + "genre": "Electronic", + "size": 792375, + "discNumber": 1, + "suffix": "wma", + "contentType": "audio/x-ms-wma", + "path": "Synthetic/Synthetic_-_Colorsmoke_EP-20k217-2007/04-Synthetic_-_BrownSmokeYSBM20k22khS.wma" + } + }, + { + "album": { + "id": "200000021", + "parent": "100000036", + "album": "Forget and Remember", + "title": "Forget and Remember", + "name": "Forget and Remember", + "isDir": true, + "coverArt": "al-200000021", + "songCount": 20, + "created": "2021-07-22T02:09:31+00:00", + "duration": 4248, + "playCount": 0, + "artistId": "100000036", + "artist": "Comfort Fit", + "year": 2005, + "genre": "Hip-Hop" + } + } + ] + } + } +} +{{< /tab >}} +{{< /tabpane >}} + +| Field | Type | Req. | OpenS. | Details | +| --- | --- | --- | --- | --- | +| `collection` | [`collectionWithItems`](../../responses/collectionwithitems) | **Yes** | | The collection | diff --git a/content/en/docs/Endpoints/getcollections.md b/content/en/docs/Endpoints/getcollections.md new file mode 100644 index 00000000..4ba6ed0e --- /dev/null +++ b/content/en/docs/Endpoints/getcollections.md @@ -0,0 +1,71 @@ +--- +title: "getCollections" +linkTitle: "getCollections [OS]" +OpenSubsonic: +- Extension +categories: +- Collections +description: > + Returns all collections a user has access to. +--- + +`http://your-server/rest/getCollections` + +Returns all collections a user has access to. + +### Parameters + +| Parameter | Req. | OpenS. | Default | Comment | +| --- | --- | --- | --- | --- | +| `username` | No | | | If specified, return collections for this user rather than for the authenticated user. The authenticated user must have admin role if this parameter is used. | + +### Example + +{{< alert color="primary" >}} `http://your-server/rest/getCollections.view?u=demo&p=demo&v=1.13.0&c=AwesomeClientName&f=json` {{< /alert >}} + +### Result + +A [`subsonic-response`](../../responses/subsonic-response) element with a top-level `collections` array of [collection](../../responses/collection) elements on success. + +{{< tabpane persist=false >}} +{{< tab header="**Example**:" disabled=true />}} +{{< tab header="OpenSubsonic" lang="json">}} +{ + "subsonic-response": { + "status": "ok", + "version": "1.16.1", + "type": "AwesomeServerName", + "serverVersion": "0.1.3 (tag)", + "openSubsonic": true, + "collections": [ + { + "id": "800000075", + "name": "testcollection", + "owner": "user", + "public": true, + "created": "2026-03-16T03:18:41+00:00", + "changed": "2026-03-16T03:18:41+00:00", + "itemCount": 3, + "readonly": true, + "validUntil": "2026-03-23T03:18:41+00:00" + }, + { + "id": "800000076", + "name": "testcollection2", + "comment" "this is another collection", + "owner": "user", + "public": false, + "itemCount": 17, + "created": "2026-03-16T03:18:41+00:00", + "changed": "2026-03-16T03:18:41+00:00", + "readonly": true + } + ] + } +} +{{< /tab >}} +{{< /tabpane >}} + +| Field | Type | Req. | OpenS. | Details | +| --- | --- | --- | --- | --- | +| `collections` | Array of [`collection`](../../responses/collection) | **Yes** | | The collections | diff --git a/content/en/docs/Endpoints/updatecollection.md b/content/en/docs/Endpoints/updatecollection.md new file mode 100644 index 00000000..bff9d688 --- /dev/null +++ b/content/en/docs/Endpoints/updatecollection.md @@ -0,0 +1,91 @@ +--- +title: "updateCollection" +linkTitle: "updateCollection [OS]" +categories: +- Collections +opensubsonic: +- Extension +description: > + Updates a collection. +--- +`http://your-server/rest/updateCollection` + +Updates a collection. Only the owner of a collection is allowed to update it. +This endpoint must be accessed using an HTTP PATCH request, only the fields specified in the request payload will have an effect on a collection. + +### Request Body + +The request payload should be provided in the body as a JSON object. + +| Parameter | Req. | OpenS. | Default | Comment | +| --- | --- | --- | --- | --- | +| `collectionId` | **Yes** | | | The collection ID. | +| `name` | No | | | The human-readable name of the collection. | +| `comment` | No | | | The collection comment. | +| `public` | No | | | `true` if the collection should be visible to all users, `false` otherwise. | +| `add` | No | | | Add the specified items to the collection. The payload is an array of [collectionItemID](../payloads/collectionitemid) objects. Items are appended to the tail of the collection. | +| `move` | No | | | Move items from the original position to a new one in the collection. The original range is specified by the `fromStart` (inclusive) and `fromEnd` (exclusive) positions. The first item in the range moves to the position specified by `to` | +| `remove` | No | | | Remove the items at the specified positions from the collection. The payload is an array of integer indices. | + +Only one of `add`, `move`, and `remove` can be non-empty in a single request. + +### Example request + +{{< alert color="primary" >}} `PATCH http://your-server/rest/updateCollection.view?u=demo&p=demo&v=1.13.0&c=AwesomeClientName&f=json` {{< /alert >}} + +{{< tabpane persist=false >}} +{{< tab header="**Body**:" disabled=true />}} +{{< tab header="Name and comment" lang="json">}} +{ + "collectionId": "23785995", + "name": "new name", + "comment": "this is a new comment" +} +{{< /tab >}} +{{< tab header="Add" lang="json">}} +{ + "collectionId": "23785995", + "add": [ + { + "type": "album", + "id": "1234" + } + ] +} +{{< /tab >}} +{{< tab header="Move" lang="json">}} +{ + "collectionId": "23785995", + "move": { + "fromStart": 0, + "fromEnd": 5, + "to": 20 + } +} +{{< /tab >}} +{{< tab header="Remove" lang="json">}} +{ + "collectionId": "23785995", + "remove": [0, 1, 2, 6] +} +{{< /tab >}} +{{< /tabpane >}} + +### Result + +An empty [`subsonic-response`](../../responses/subsonic-response) element on success. + +{{< tabpane persist=false >}} +{{< tab header="**Example**:" disabled=true />}} +{{< tab header="OpenSubsonic" lang="json">}} +{ + "subsonic-response": { + "status": "ok", + "version": "1.16.1", + "type": "AwesomeServerName", + "serverVersion": "0.1.3 (tag)", + "openSubsonic": true + } +} +{{< /tab >}} +{{< /tabpane >}} diff --git a/content/en/docs/Extensions/collections.md b/content/en/docs/Extensions/collections.md new file mode 100644 index 00000000..bb673ff5 --- /dev/null +++ b/content/en/docs/Extensions/collections.md @@ -0,0 +1,25 @@ +--- +title: "Collections" +linkTitle: "Collections" +OpenSubsonic: +- Extension +description: > + Add support for collections. +--- + +**OpenSubsonic version**: [1](../../opensubsonic-versions) + +**OpenSubsonic extension name**: `collections` (As returned by [`getOpenSubsonicExtensions`](../../endpoints/getopensubsonicextensions)) + +This extension allows users to create and manage collections. +A collection is an ordered list of items - songs, artist, albums, playlists. + +## Version 1 + +Servers that support this extension provide the following endpoints: + +- [`getCollections`](../../endpoints/getCollections) +- [`getCollection`](../../endpoints/getCollection) +- [`createCollection`](../../endpoints/createCollection) +- [`updateCollection`](../../endpoints/updateCollection) +- [`deleteCollection`](../../endpoints/deleteCollection) diff --git a/content/en/docs/Payloads/CollectionItemID.md b/content/en/docs/Payloads/CollectionItemID.md new file mode 100644 index 00000000..36d216fe --- /dev/null +++ b/content/en/docs/Payloads/CollectionItemID.md @@ -0,0 +1,31 @@ +--- +title: "collectionItemID" +linkTitle: "collectionItemID [OS]" +categories: +- Collections +opensubsonic: +- Extension +description: > + An identifier for a collection item to be added. +--- + +{{< tabpane persist=false >}} +{{< tab header="**Example**:" disabled=true />}} +{{< tab header="Song" lang="json">}} +{ + "type": "song", + "id": "20" +} +{{< /tab >}} +{{< tab header="Album" lang="json">}} +{ + "type": "album", + "id": "30" +} +{{< /tab >}} +{{< /tabpane >}} + +| Field | Type | Req. | OpenS. | Details | +| --- | --- | --- | --- | --- | +| `type` | `string` | **Yes** | | One of: song, album, artist, playlist | +| `id` | `string` | **Yes** | | An identifier of the item | diff --git a/content/en/docs/Responses/collection.md b/content/en/docs/Responses/collection.md new file mode 100644 index 00000000..440e95c8 --- /dev/null +++ b/content/en/docs/Responses/collection.md @@ -0,0 +1,47 @@ +--- +title: "collection" +linkTitle: "collection [OS]" +categories: +- Collections +opensubsonic: +- Extension +description: > + A collection, as returned by the [getCollections](../../endpoints/getcollections) endpoint. +--- + +Does not contain the items in the collection, look at [collectionWithItems](../../responses/collectionwithitems) instead. + +{{< tabpane persist=false >}} +{{< tab header="**Example**:" disabled=true />}} +{{< tab header="Collection" lang="json">}} +{ + "id": "800000075", + "name": "testcollection", + "owner": "user", + "public": true, + "created": "2026-03-16T03:18:41+00:00", + "changed": "2026-03-16T03:18:41+00:00", + "itemCount": 3, + "readonly": true, + "validUntil": "2026-03-23T03:18:41+00:00" +} +{{< /tab >}} +{{< /tabpane >}} + +| Field | Type | Req. | OpenS. | Details | +| --- | --- | --- | --- | --- | +| `id` | `string` | **Yes** | | Id of the collection | +| `name` | `string` | **Yes** | | Name of the collection | +| `comment` | `string` | No| | A commnet | +| `owner` | `string` | No | | Owner of the collection | +| `public` | `boolean` | No| | Is the collection public | +| `itemCount` | `int` | **Yes** | | number of items | +| `created` | `string` | **Yes** | | Creation date [ISO 8601] | +| `changed` | `string` | **Yes** | | Last changed date [ISO 8601] | +| `allowedUser` | Array of `string` | No | | A list of allowed usernames | +| `readonly` | `boolean` | No | | If true the collection cannot be edited by the current user | +| `validUntil` | `string` | No | | Date the collection contents are considered valid until [ISO 8601] | + +When `readonly` is true, clients should hide or disable UI actions that modify the collection. The value should reflect the current authenticated user's access level. When omitted, clients should assume the collection is editable (`false`). + +The `validUntil` field indicates how long the collection contents can be treated as fresh, inspired by HTTP caching semantics. Clients may use this to determine when to refresh the collection data. An empty or absent value indicates no caching guarantee; clients should refresh the collection data on each access. diff --git a/content/en/docs/Responses/collectionItem.md b/content/en/docs/Responses/collectionItem.md new file mode 100644 index 00000000..dc0f614c --- /dev/null +++ b/content/en/docs/Responses/collectionItem.md @@ -0,0 +1,268 @@ +--- +title: "collectionItem" +linkTitle: "collectionItem [OS]" +categories: +- Collections +opensubsonic: +- Extension +description: > + A collection item. +--- + +An item in a collection is a wrapper around one of the following types: + +- [Child](../responses/child) +- [AlbumID3](../albumid3/) +- [ArtistID3](../artistid3/) +- [Playlist](../playlist/) + +{{< tabpane persist=false >}} +{{< tab header="**Example**:" disabled=true />}} +{{< tab header="Song (child)" lang="json">}} +{ + "song": { + "id": "082f435a363c32c57d5edb6a678a28d4", + "parent": "e8a0685e3f3ec6f251649af2b58b8617", + "isDir": false, + "title": "\"polar expedition\"", + "album": "Live at The Casbah - 2005-04-29", + "artist": "The New Deal", + "track": 4, + "year": 2005, + "coverArt": "mf-082f435a363c32c57d5edb6a678a28d4_6410b3ce", + "size": 19866778, + "contentType": "audio/flac", + "suffix": "flac", + "starred": "2023-03-27T09:45:27Z", + "duration": 178, + "bitRate": 880, + "bitDepth": 16, + "samplingRate": 44100, + "channelCount": 2, + "path": "The New Deal/Live at The Casbah - 2005-04-29/04 - \"polar expedition\".flac", + "playCount": 8, + "played": "2023-03-26T22:27:46Z", + "discNumber": 1, + "created": "2023-03-14T17:51:22.112827504Z", + "albumId": "e8a0685e3f3ec6f251649af2b58b8617", + "artistId": "97e0398acf63f9fb930d7d4ce209a52b", + "type": "music", + "mediaType": "song", + "isVideo": false, + "bpm": 134, + "comment": "This is a song comment", + "sortName": "Polar expedition", + "musicBrainzId": "189002e7-3285-4e2e-92a3-7f6c30d407a2", + "isrc": [ + "USSM18300073", + "DELV42300297", + "DEE868300011", + "DEE868300007" + ], + "genres": [ + { + "name": "Hip-Hop" + }, + { + "name": "East coast" + } + ], + "artists": [ + { + "id": "ar-1", + "name": "Artist 1" + }, + { + "id": "ar-2", + "name": "Artist 2" + } + ], + "displayArtist": "Artist 1 feat. Artist 2", + "albumArtists": [ + { + "id": "ar-6", + "name": "Artist 6" + }, + { + "id": "ar-7", + "name": "Artist 7" + } + ], + "displayAlbumArtist": "Artist 6 & Artist 7", + "contributors": [ + { + "role": "composer", + "artist": { + "id": "ar-3", + "name": "Artist 3" + } + }, + { + "role": "composer", + "artist": { + "id": "ar-4", + "name": "Artist 4" + } + }, + { + "role": "lyricist", + "artist": { + "id": "ar-5", + "name": "Artist 5" + } + }, + { + "role": "performer", + "subRole": "Bass", + "artist": { + "id": "ar-5", + "name": "Artist 5" + } + } + ], + "displayComposer": "Artist 3, Artist 4", + "moods": [ + "slow", + "cool" + ], + "explicitStatus": "explicit", + "replayGain": { + "trackGain": 0.1, + "albumGain": 1.1, + "trackPeak": 9.2, + "albumPeak": 9, + "baseGain": 0 + }, + "works": [ + { + "name": "Symphony No. 5 in C minor, Op. 67", + "musicBrainzId": "d03bff61-26fc-301b-98ac-4d8e85771cbc" + } + ], + "movements": [ + { + "name": "Andante con moto", + "number": 2, + "count": 4 + } + ], + "groupings": ["Soundtrack", "Live"] + } +} +{{< /tab >}} +{{< tab header="Album" lang="json">}} +{ + "album": { + "id": "ad0f112b6dcf83de5e9cae85d07f0d35", + "name": "8-bit lagerfeuer", + "version": "Deluxe Edition", + "artist": "pornophonique", + "year": 2007, + "coverArt": "al-ad0f112b6dcf83de5e9cae85d07f0d35_640a93a8", + "starred": "2023-03-22T01:51:06Z", + "duration": 1954, + "playCount": 97, + "genre": "Hip-Hop", + "created": "2023-03-10T02:19:35.784818075Z", + "artistId": "91c3901ac465b9efc439e4be4270c2b6", + "songCount": 8, + "played": "2023-03-28T00:45:13Z", + "userRating": 4, + "recordLabels": [ + { + "name": "Sony" + } + ], + "musicBrainzId": "189002e7-3285-4e2e-92a3-7f6c30d407a2", + "genres": [ + { + "name": "Hip-Hop" + }, + { + "name": "East coast" + } + ], + "artists": [ + { + "id": "ar-1", + "name": "Artist 1" + }, + { + "id": "ar-2", + "name": "Artist 2" + } + ], + "displayArtist": "Artist 1 feat. Artist 2", + "releaseTypes": [ + "Album", + "Remixes" + ], + "moods": [ + "slow", + "cool" + ], + "sortName": "lagerfeuer (8-bit)", + "originalReleaseDate": { + "year": 2001, + "month": 3, + "day": 10 + }, + "releaseDate": { + "year": 2001, + "month": 3, + "day": 10 + }, + "isCompilation": false, + "explicitStatus": "explicit", + "discTitles": [ + { + "disc": 0, + "title": "Disc 0 title" + "coverArt": "42" + }, + { + "disc": 2, + "title": "Disc 1 title" + "coverArt": "6547" + } + ] + } +} +{{< /tab >}} +{{< tab header="Artist" lang="json">}} +{ + "artist": { + "id": "37ec820ca7193e17040c98f7da7c4b51", + "name": "2 Mello", + "coverArt": "ar-37ec820ca7193e17040c98f7da7c4b51_0", + "albumCount": 1, + "userRating": 5, + "artistImageUrl": "https://demo.org/image.jpg", + "starred": "2017-04-11T10:42:50.842Z", + "musicBrainzId": "189002e7-3285-4e2e-92a3-7f6c30d407a2", + "sortName": "Mello (2)", + "roles": [ + "artist", + "albumartist", + "composer" + ] + } +} +{{< /tab >}} +{{< tab header="Playlist" lang="json">}} +{ + "playlist": { + "id": "800000075", + "name": "testplaylist", + "owner": "user", + "public": true, + "created": "2023-03-16T03:18:41+00:00", + "changed": "2023-03-16T03:18:41+00:00", + "songCount": 1, + "duration": 304, + "readonly": true, + "validUntil": "2023-03-23T03:18:41+00:00" + } +} +{{< /tab >}} +{{< /tabpane >}} diff --git a/content/en/docs/Responses/collectionWithItems.md b/content/en/docs/Responses/collectionWithItems.md new file mode 100644 index 00000000..00385a1d --- /dev/null +++ b/content/en/docs/Responses/collectionWithItems.md @@ -0,0 +1,97 @@ +--- +title: "collectionWithItems" +linkTitle: "collectionWithItems [OS]" +categories: +- Collections +opensubsonic: +- Extension +description: > + A collection with items, as returned by the [getCollection](../../endpoints/getcollection) endpoint. +--- + +{{< tabpane persist=false >}} +{{< tab header="**Example**:" disabled=true />}} +{{< tab header="Collection" lang="json">}} +{ + "id": "800000075", + "name": "testcollection", + "owner": "user", + "public": true, + "created": "2026-03-16T03:18:41+00:00", + "changed": "2026-03-16T03:18:41+00:00", + "itemCount": 3, + "readonly": true, + "validUntil": "2026-03-23T03:18:41+00:00", + "items": [ + { + "song": { + "id": "300000060", + "parent": "200000002", + "title": "BrownSmoke", + "isDir": false, + "isVideo": false, + "type": "music", + "albumId": "200000002", + "album": "Colorsmoke EP", + "artistId": "100000002", + "artist": "Synthetic", + "coverArt": "300000060", + "duration": 304, + "bitRate": 20, + "bitDepth": 16, + "samplingRate": 44100, + "channelCount": 2, + "userRating": 5, + "averageRating": 5, + "track": 4, + "year": 2007, + "genre": "Electronic", + "size": 792375, + "discNumber": 1, + "suffix": "wma", + "contentType": "audio/x-ms-wma", + "path": "Synthetic/Synthetic_-_Colorsmoke_EP-20k217-2007/04-Synthetic_-_BrownSmokeYSBM20k22khS.wma" + } + }, + { + "album": { + "id": "200000021", + "parent": "100000036", + "album": "Forget and Remember", + "title": "Forget and Remember", + "name": "Forget and Remember", + "isDir": true, + "coverArt": "al-200000021", + "songCount": 20, + "created": "2021-07-22T02:09:31+00:00", + "duration": 4248, + "playCount": 0, + "artistId": "100000036", + "artist": "Comfort Fit", + "year": 2005, + "genre": "Hip-Hop" + } + } + ] +} +{{< /tab >}} +{{< /tabpane >}} + +| Field | Type | Req. | OpenS. | Details | +| --- | --- | --- | --- | --- | +| `id` | `string` | **Yes** | | Id of the collection | +| `name` | `string` | **Yes** | | Name of the collection | +| `comment` | `string` | No| | A commnet | +| `owner` | `string` | No | | Owner of the collection | +| `public` | `boolean` | No| | Is the collection public | +| `itemCount` | `int` | **Yes** | | number of items | +| `created` | `string` | **Yes** | | Creation date [ISO 8601] | +| `changed` | `string` | **Yes** | | Last changed date [ISO 8601] | +| `allowedUser` | Array of `string` | No | | A list of allowed usernames | +| `readonly` | `boolean` | No | | If true the collection cannot be edited by the current user | +| `validUntil` | `string` | No | | Date the collection contents are considered valid until [ISO 8601] | +| `items` | Array of [`collectionItem`](../collectionitem) | **Yes** | | The list of items | + +When `readonly` is true, clients should hide or disable UI actions that modify the collection. This is useful for server-generated collections like smart collections, recommendations, or curated system lists. The value should reflect the current authenticated user's access level. When omitted, clients should assume the collection is editable (`false`). + +The `validUntil` field indicates how long the collection contents can be treated as fresh, inspired by HTTP caching semantics. Clients may use this to determine when to refresh the collection data. An empty or absent value indicates no caching guarantee; clients should refresh the collection data on each access. diff --git a/openapi/endpoints/createCollection.json b/openapi/endpoints/createCollection.json new file mode 100644 index 00000000..821155ba --- /dev/null +++ b/openapi/endpoints/createCollection.json @@ -0,0 +1,68 @@ +{ + "post": { + "summary": "Creates a collection.", + "description": "Creates a collection.", + "operationId": "createCollection", + "tags": [ + "Collections", + "Extension" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The human-readable name of the collection." + }, + "comment": { + "type": "string", + "description": "The collection comment." + }, + "public": { + "description": "`true` if the collection should be visible to all users, `false` otherwise.", + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "../schemas/CollectionItemId.json" + }, + "description": "IDs of items in the collection." + } + }, + "required": [ + "name" + ] + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful or failed response", + "content": { + "application/json": { + "schema": { + "$ref": "./createCollection/CreateCollectionResponse.json" + } + } + } + }, + "404": { + "description": "Extension not supported." + }, + "405": { + "$ref": "../responses/HTTPFormPostNotSupported.json" + } + }, + "externalDocs": { + "description": "createCollection", + "url": "https://opensubsonic.netlify.app/docs/endpoints/createcollection/" + } + } +} diff --git a/openapi/endpoints/createCollection/CreateCollectionResponse.json b/openapi/endpoints/createCollection/CreateCollectionResponse.json new file mode 100644 index 00000000..c873b21a --- /dev/null +++ b/openapi/endpoints/createCollection/CreateCollectionResponse.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "description": "A subsonic-response element with a nested collection element on success.", + "properties": { + "subsonic-response": { + "oneOf": [ + { + "$ref": "./CreateCollectionSuccessResponse.json" + }, + { + "$ref": "../../schemas/SubsonicResponse/SubsonicFailureResponse.json" + } + ] + } + }, + "externalDocs": { + "description": "CreateCollectionResponse", + "url": "https://opensubsonic.netlify.app/docs/endpoints/createcollection/" + } +} diff --git a/openapi/endpoints/createCollection/CreateCollectionSuccessResponse.json b/openapi/endpoints/createCollection/CreateCollectionSuccessResponse.json new file mode 100644 index 00000000..f7e164c5 --- /dev/null +++ b/openapi/endpoints/createCollection/CreateCollectionSuccessResponse.json @@ -0,0 +1,18 @@ +{ + "allOf": [ + { + "$ref": "../../schemas/SubsonicResponse/SubsonicSuccessResponse.json" + }, + { + "type": "object", + "properties": { + "collection": { + "$ref": "../../schemas/CollectionWithItems.json" + } + }, + "required": [ + "collection" + ] + } + ] +} diff --git a/openapi/endpoints/deleteCollection.json b/openapi/endpoints/deleteCollection.json new file mode 100644 index 00000000..fbb9b5c9 --- /dev/null +++ b/openapi/endpoints/deleteCollection.json @@ -0,0 +1,34 @@ +{ + "delete": { + "summary": "Deletes a saved collection.", + "description": "Deletes a saved collection.", + "operationId": "deleteCollection", + "tags": [ + "Collections", + "Extension" + ], + "parameters": [ + { + "name": "id", + "in": "query", + "description": "ID of the collection to delete, as obtained by `getCollections`.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "../responses/EmptySubsonicResponse.json" + }, + "404": { + "description": "Collection does not exist, or extension not supported." + } + }, + "externalDocs": { + "description": "deleteCollection", + "url": "https://opensubsonic.netlify.app/docs/endpoints/deletecollection/" + } + } +} diff --git a/openapi/endpoints/getCollection.json b/openapi/endpoints/getCollection.json new file mode 100644 index 00000000..580614c9 --- /dev/null +++ b/openapi/endpoints/getCollection.json @@ -0,0 +1,41 @@ +{ + "get": { + "summary": "Get a collection", + "description": "Get a collection", + "operationId": "getCollection", + "tags": [ + "Collections", + "Extension" + ], + "parameters": [ + { + "name": "id", + "in": "query", + "description": "ID of the collection to return, as obtained by `getCollections`.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful or failed response", + "content": { + "application/json": { + "schema": { + "$ref": "./getCollection/GetCollectionResponse.json" + } + } + } + }, + "404": { + "description": "Collection does not exist, or extension not supported." + } + }, + "externalDocs": { + "description": "getCollection", + "url": "https://opensubsonic.netlify.app/docs/endpoints/getcollection/" + } + } +} diff --git a/openapi/endpoints/getCollection/GetCollectionResponse.json b/openapi/endpoints/getCollection/GetCollectionResponse.json new file mode 100644 index 00000000..0b11a40d --- /dev/null +++ b/openapi/endpoints/getCollection/GetCollectionResponse.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "description": "A subsonic-response element with a nested collection element on success.", + "properties": { + "subsonic-response": { + "oneOf": [ + { + "$ref": "./GetCollectionSuccessResponse.json" + }, + { + "$ref": "../../schemas/SubsonicResponse/SubsonicFailureResponse.json" + } + ] + } + }, + "externalDocs": { + "description": "GetCollectionResponse", + "url": "https://opensubsonic.netlify.app/docs/endpoints/getcollection/" + } +} diff --git a/openapi/endpoints/getCollection/GetCollectionSuccessResponse.json b/openapi/endpoints/getCollection/GetCollectionSuccessResponse.json new file mode 100644 index 00000000..f7e164c5 --- /dev/null +++ b/openapi/endpoints/getCollection/GetCollectionSuccessResponse.json @@ -0,0 +1,18 @@ +{ + "allOf": [ + { + "$ref": "../../schemas/SubsonicResponse/SubsonicSuccessResponse.json" + }, + { + "type": "object", + "properties": { + "collection": { + "$ref": "../../schemas/CollectionWithItems.json" + } + }, + "required": [ + "collection" + ] + } + ] +} diff --git a/openapi/endpoints/getCollections.json b/openapi/endpoints/getCollections.json new file mode 100644 index 00000000..8933eea7 --- /dev/null +++ b/openapi/endpoints/getCollections.json @@ -0,0 +1,41 @@ +{ + "get": { + "summary": "Get all collections a user has access to.", + "description": "Get all collections a user has access to.", + "operationId": "getCollections", + "tags": [ + "Collections", + "Extension" + ], + "parameters": [ + { + "name": "username", + "in": "query", + "description": "If specified, return collections for this user rather than for the authenticated user. The authenticated user must have admin role if this parameter is used.", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful or failed response", + "content": { + "application/json": { + "schema": { + "$ref": "./getCollections/GetCollectionsResponse.json" + } + } + } + }, + "404": { + "description": "User does not exist, or extension not supported." + } + }, + "externalDocs": { + "description": "getCollections", + "url": "https://opensubsonic.netlify.app/docs/endpoints/getcollections/" + } + } +} diff --git a/openapi/endpoints/getCollections/GetCollectionsResponse.json b/openapi/endpoints/getCollections/GetCollectionsResponse.json new file mode 100644 index 00000000..83d245ce --- /dev/null +++ b/openapi/endpoints/getCollections/GetCollectionsResponse.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "description": "A subsonic-response element with a nested `collections` element on success.", + "properties": { + "subsonic-response": { + "oneOf": [ + { + "$ref": "./GetCollectionsSuccessResponse.json" + }, + { + "$ref": "../../schemas/SubsonicResponse/SubsonicFailureResponse.json" + } + ] + } + }, + "externalDocs": { + "description": "GetCollectionsResponse", + "url": "https://opensubsonic.netlify.app/docs/endpoints/getcollections/" + } +} diff --git a/openapi/endpoints/getCollections/GetCollectionsSuccessResponse.json b/openapi/endpoints/getCollections/GetCollectionsSuccessResponse.json new file mode 100644 index 00000000..a6b9f035 --- /dev/null +++ b/openapi/endpoints/getCollections/GetCollectionsSuccessResponse.json @@ -0,0 +1,22 @@ +{ + "allOf": [ + { + "$ref": "../../schemas/SubsonicResponse/SubsonicSuccessResponse.json" + }, + { + "type": "object", + "properties": { + "collections": { + "type": "array", + "items": { + "$ref": "../../schemas/Collection.json" + }, + "description": "The collections" + } + }, + "required": [ + "collections" + ] + } + ] +} diff --git a/openapi/endpoints/updateCollection.json b/openapi/endpoints/updateCollection.json new file mode 100644 index 00000000..fb35da71 --- /dev/null +++ b/openapi/endpoints/updateCollection.json @@ -0,0 +1,82 @@ +{ + "patch": { + "summary": "Updates a collection.", + "description": "Updates a collection. Only the owner of a collection is allowed to update it. The endpoint follows the PATCH semantics, so only the fields specified in the request payload will have an effect on a collection.", + "operationId": "updateCollection", + "tags": [ + "Collections", + "Extension" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "collectionId": { + "description": "The collection ID.", + "type": "string" + }, + "name": { + "description": "The human-readable name of the collection.", + "type": "string" + }, + "comment": { + "description": "The collection comment.", + "type": "string" + }, + "public": { + "description": "`true` if the collection should be visible to all users, `false` otherwise.", + "type": "boolean" + }, + "add": { + "description": "Add the specified items to the collection. Multiple items can be specified.", + "type": "array", + "items": { + "$ref": "../schemas/CollectionItemId.json" + } + }, + "move": { + "description": "Move items from the original position to a new one in the collection. The original range is specified by the `fromStart` (inclusive) and `fromEnd` (exclusive) positions. The first item in the range moves to the position specified by `to`", + "type": "object", + "required": ["fromStart", "fromEnd", "to"], + "properties": { + "fromStart": {"type": "integer"}, + "fromEnd": {"type": "integer"}, + "to": {"type": "integer"} + } + }, + "remove": { + "description": "Remove the item at this position from the collection. Multiple items can be specified.", + "type": "array", + "items": { + "type": "integer" + } + } + }, + "required": [ + "collectionId" + ] + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "$ref": "../responses/EmptySubsonicResponse.json" + }, + "404": { + "description": "Collection does not exist, or extension not supported." + }, + "405": { + "$ref": "../responses/HTTPFormPostNotSupported.json" + } + }, + "externalDocs": { + "description": "updateCollection", + "url": "https://opensubsonic.netlify.app/docs/endpoints/updatecollection/" + } + } +} diff --git a/openapi/openapi.json b/openapi/openapi.json index 9d0da653..9d5948e4 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -58,6 +58,9 @@ { "name": "Playlists" }, + { + "name": "Collections" + }, { "name": "Lists" }, @@ -117,6 +120,9 @@ "/rest/createPlaylist": { "$ref": "./endpoints/createPlaylist.json" }, + "/rest/createCollection": { + "$ref": "./endpoints/createCollection.json" + }, "/rest/createPodcastChannel": { "$ref": "./endpoints/createPodcastChannel.json" }, @@ -135,6 +141,9 @@ "/rest/deletePlaylist": { "$ref": "./endpoints/deletePlaylist.json" }, + "/rest/deleteCollection": { + "$ref": "./endpoints/deleteCollection.json" + }, "/rest/deletePodcastChannel": { "$ref": "./endpoints/deletePodcastChannel.json" }, @@ -237,6 +246,12 @@ "/rest/getPlaylists": { "$ref": "./endpoints/getPlaylists.json" }, + "/rest/getCollection": { + "$ref": "./endpoints/getCollection.json" + }, + "/rest/getCollections": { + "$ref": "./endpoints/getCollections.json" + }, "/rest/getPlayQueue": { "$ref": "./endpoints/getPlayQueue.json" }, @@ -357,6 +372,9 @@ "/rest/updatePlaylist": { "$ref": "./endpoints/updatePlaylist.json" }, + "/rest/updateCollection": { + "$ref": "./endpoints/updateCollection.json" + }, "/rest/updateShare": { "$ref": "./endpoints/updateShare.json" }, @@ -479,6 +497,24 @@ "CreatePlaylistResponse": { "$ref": "./endpoints/createPlaylist/CreatePlaylistResponse.json" }, + "Collection": { + "$ref": "./schemas/Collection.json" + }, + "CollectionItem": { + "$ref": "./schemas/CollectionItem.json" + }, + "CollectionItemId": { + "$ref": "./schemas/CollectionItemId.json" + }, + "CollectionWithItems": { + "$ref": "./schemas/CollectionWithItems.json" + }, + "CreateCollectionSuccessResponse": { + "$ref": "./endpoints/createCollection/CreateCollectionSuccessResponse.json" + }, + "CreateCollectionResponse": { + "$ref": "./endpoints/createCollection/CreateCollectionResponse.json" + }, "Share": { "$ref": "./schemas/Share.json" }, @@ -758,6 +794,18 @@ "GetPlaylistsResponse": { "$ref": "./endpoints/getPlaylists/GetPlaylistsResponse.json" }, + "GetCollectionSuccessResponse": { + "$ref": "./endpoints/getCollection/GetCollectionSuccessResponse.json" + }, + "GetCollectionResponse": { + "$ref": "./endpoints/getCollection/GetCollectionResponse.json" + }, + "GetCollectionsSuccessResponse": { + "$ref": "./endpoints/getCollections/GetCollectionsSuccessResponse.json" + }, + "GetCollectionsResponse": { + "$ref": "./endpoints/getCollections/GetCollectionsResponse.json" + }, "PlayQueue": { "$ref": "./schemas/PlayQueue.json" }, diff --git a/openapi/schemas/Collection.json b/openapi/schemas/Collection.json new file mode 100644 index 00000000..589c7656 --- /dev/null +++ b/openapi/schemas/Collection.json @@ -0,0 +1,67 @@ +{ + "type": "object", + "description": "Collection.", + "properties": { + "id": { + "type": "string", + "description": "Id of the collection" + }, + "name": { + "type": "string", + "description": "Name of the collection" + }, + "comment": { + "type": "string", + "description": "A comment" + }, + "owner": { + "type": "string", + "description": "Owner of the collection" + }, + "public": { + "type": "boolean", + "description": "Is the collection public" + }, + "itemCount": { + "type": "integer", + "description": "Number of items" + }, + "created": { + "type": "string", + "format": "date-time", + "description": "Creation date [ISO 8601]" + }, + "changed": { + "type": "string", + "format": "date-time", + "description": "Last changed date [ISO 8601]" + }, + "allowedUser": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of allowed usernames" + }, + "readonly": { + "type": "boolean", + "description": "If true the collection cannot be edited by the current user" + }, + "validUntil": { + "type": "string", + "format": "date-time", + "description": "Date the collection contents are considered valid until [ISO 8601]. Empty or absent means no caching guarantee." + } + }, + "required": [ + "id", + "name", + "itemCount", + "created", + "changed" + ], + "externalDocs": { + "description": "Collection", + "url": "https://opensubsonic.netlify.app/docs/responses/collection/" + } +} diff --git a/openapi/schemas/CollectionItem.json b/openapi/schemas/CollectionItem.json new file mode 100644 index 00000000..2e3afc44 --- /dev/null +++ b/openapi/schemas/CollectionItem.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "description": "Item that can be part of a collection.", + "oneOf": [ + { + "type": "object", + "properties": { + "song": { + "$ref": "Child.json" + } + } + }, + { + "type": "object", + "properties": { + "album": { + "$ref": "AlbumID3.json" + } + } + }, + { + "type": "object", + "properties": { + "artist": { + "$ref": "ArtistID3.json" + } + } + }, + { + "type": "object", + "properties": { + "playlist": { + "$ref": "Playlist.json" + } + } + } + ] +} diff --git a/openapi/schemas/CollectionItemId.json b/openapi/schemas/CollectionItemId.json new file mode 100644 index 00000000..aa004720 --- /dev/null +++ b/openapi/schemas/CollectionItemId.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "description": "Collection item identifier.", + "properties": { + "type": { + "type": "string", + "description": "Type of the collection item.", + "enum": [ + "song", + "album", + "artist", + "playlist" + ] + }, + "id": { + "type": "string", + "description": "Id of the item." + } + }, + "required": [ + "type", + "id" + ], + "externalDocs": { + "description": "CollectionItemId", + "url": "https://opensubsonic.netlify.app/docs/payloads/collectionitemid/" + } +} diff --git a/openapi/schemas/CollectionWithItems.json b/openapi/schemas/CollectionWithItems.json new file mode 100644 index 00000000..cb120cad --- /dev/null +++ b/openapi/schemas/CollectionWithItems.json @@ -0,0 +1,26 @@ +{ + "allOf": [ + { + "$ref": "./Collection.json" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "./CollectionItem.json" + }, + "description": "The list of items in the collection" + } + }, + "required": [ + "items" + ], + "externalDocs": { + "description": "CollectionWithItems", + "url": "https://opensubsonic.netlify.app/docs/responses/collectionwithitems/" + } + } + ] +} diff --git a/openapi/schemas/Collections.json b/openapi/schemas/Collections.json new file mode 100644 index 00000000..7a8912ac --- /dev/null +++ b/openapi/schemas/Collections.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "description": "Collections.", + "externalDocs": { + "description": "Collections", + "url": "https://opensubsonic.netlify.app/docs/responses/collections/" + }, + "properties": { + "collection": { + "type": "array", + "items": { + "$ref": "./Collection.json" + }, + "description": "The collections" + } + } +} From d9a6e1e827a2f55b6365fbda61a8504e9d755721 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Fri, 29 May 2026 13:45:30 +0200 Subject: [PATCH 02/14] collections: update HTTP method usage --- content/en/docs/Endpoints/deletecollection.md | 3 +-- content/en/docs/Endpoints/updatecollection.md | 5 +++-- openapi/endpoints/deleteCollection.json | 2 +- openapi/endpoints/updateCollection.json | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/content/en/docs/Endpoints/deletecollection.md b/content/en/docs/Endpoints/deletecollection.md index 2b92c074..111e5980 100644 --- a/content/en/docs/Endpoints/deletecollection.md +++ b/content/en/docs/Endpoints/deletecollection.md @@ -11,7 +11,6 @@ description: > `http://your-server/rest/deleteCollection` Deletes a collection. -This endpoint must be accessed using an HTTP DELETE request ### Parameters @@ -21,7 +20,7 @@ This endpoint must be accessed using an HTTP DELETE request ### Example -{{< alert color="primary" >}} `DELETE http://your-server/rest/deleteCollection.view?id=123&u=demo&p=demo&v=1.13.0&c=AwesomeClientName&f=json` {{< /alert >}} +{{< alert color="primary" >}} `http://your-server/rest/deleteCollection.view?id=123&u=demo&p=demo&v=1.13.0&c=AwesomeClientName&f=json` {{< /alert >}} ### Result diff --git a/content/en/docs/Endpoints/updatecollection.md b/content/en/docs/Endpoints/updatecollection.md index bff9d688..a4203424 100644 --- a/content/en/docs/Endpoints/updatecollection.md +++ b/content/en/docs/Endpoints/updatecollection.md @@ -11,7 +11,8 @@ description: > `http://your-server/rest/updateCollection` Updates a collection. Only the owner of a collection is allowed to update it. -This endpoint must be accessed using an HTTP PATCH request, only the fields specified in the request payload will have an effect on a collection. +This endpoint must be accessed using an HTTP POST request. +Only the fields specified in the request payload will have an effect on a collection. ### Request Body @@ -31,7 +32,7 @@ Only one of `add`, `move`, and `remove` can be non-empty in a single request. ### Example request -{{< alert color="primary" >}} `PATCH http://your-server/rest/updateCollection.view?u=demo&p=demo&v=1.13.0&c=AwesomeClientName&f=json` {{< /alert >}} +{{< alert color="primary" >}} `POST http://your-server/rest/updateCollection.view?u=demo&p=demo&v=1.13.0&c=AwesomeClientName&f=json` {{< /alert >}} {{< tabpane persist=false >}} {{< tab header="**Body**:" disabled=true />}} diff --git a/openapi/endpoints/deleteCollection.json b/openapi/endpoints/deleteCollection.json index fbb9b5c9..1b43ec5c 100644 --- a/openapi/endpoints/deleteCollection.json +++ b/openapi/endpoints/deleteCollection.json @@ -1,5 +1,5 @@ { - "delete": { + "get": { "summary": "Deletes a saved collection.", "description": "Deletes a saved collection.", "operationId": "deleteCollection", diff --git a/openapi/endpoints/updateCollection.json b/openapi/endpoints/updateCollection.json index fb35da71..422342f5 100644 --- a/openapi/endpoints/updateCollection.json +++ b/openapi/endpoints/updateCollection.json @@ -1,7 +1,7 @@ { - "patch": { + "post": { "summary": "Updates a collection.", - "description": "Updates a collection. Only the owner of a collection is allowed to update it. The endpoint follows the PATCH semantics, so only the fields specified in the request payload will have an effect on a collection.", + "description": "Updates a collection. Only the owner of a collection is allowed to update it. Only the fields specified in the request payload will have an effect on a collection.", "operationId": "updateCollection", "tags": [ "Collections", From 39e1276d2bd6095b11ddfcd1a44ceee40868b94f Mon Sep 17 00:00:00 2001 From: Yuriy Date: Fri, 29 May 2026 14:08:53 +0200 Subject: [PATCH 03/14] collections: add item types - genre - internet radio station - podcast episode - podcast --- content/en/docs/Endpoints/createcollection.md | 13 ++- content/en/docs/Endpoints/getcollection.md | 9 ++- content/en/docs/Extensions/collections.md | 2 +- content/en/docs/Payloads/CollectionItemID.md | 15 +++- content/en/docs/Responses/collectionItem.md | 64 +++++++++++++++ .../en/docs/Responses/collectionWithItems.md | 7 ++ openapi/endpoints/createCollection.json | 2 +- openapi/schemas/CollectionItem.json | 32 ++++++++ openapi/schemas/CollectionItemId.json | 79 +++++++++++++------ 9 files changed, 193 insertions(+), 30 deletions(-) diff --git a/content/en/docs/Endpoints/createcollection.md b/content/en/docs/Endpoints/createcollection.md index 92b59e03..41c2eb62 100644 --- a/content/en/docs/Endpoints/createcollection.md +++ b/content/en/docs/Endpoints/createcollection.md @@ -42,6 +42,10 @@ The request payload should be provided in the body as a JSON object. { "type": "album", "id": "200000021" + }, + { + "type": "genre", + "name": "vaporwave" } ] } @@ -68,7 +72,7 @@ A [`subsonic-response`](../../responses/subsonic-response) element with a top-le "comment" "this is a collection", "owner": "user", "public": false, - "itemCount": 2, + "itemCount": 3, "created": "2023-03-16T03:18:41+00:00", "changed": "2023-03-16T03:18:41+00:00", "items": [ @@ -120,6 +124,13 @@ A [`subsonic-response`](../../responses/subsonic-response) element with a top-le "year": 2005, "genre": "Hip-Hop" } + }, + { + "genre": { + "value": "vaporwave", + "songCount": 6, + "albumCount": 1 + } } ] } diff --git a/content/en/docs/Endpoints/getcollection.md b/content/en/docs/Endpoints/getcollection.md index ed54e73e..be62a031 100644 --- a/content/en/docs/Endpoints/getcollection.md +++ b/content/en/docs/Endpoints/getcollection.md @@ -43,7 +43,7 @@ A [`subsonic-response`](../../responses/subsonic-response) element with a top-le "comment" "this is a collection", "owner": "user", "public": true, - "itemCount": 2, + "itemCount": 3, "created": "2023-03-16T03:18:41+00:00", "changed": "2023-03-16T03:18:41+00:00", "items": [ @@ -95,6 +95,13 @@ A [`subsonic-response`](../../responses/subsonic-response) element with a top-le "year": 2005, "genre": "Hip-Hop" } + }, + { + "genre": { + "value": "vaporwave", + "songCount": 6, + "albumCount": 1 + } } ] } diff --git a/content/en/docs/Extensions/collections.md b/content/en/docs/Extensions/collections.md index bb673ff5..8743359f 100644 --- a/content/en/docs/Extensions/collections.md +++ b/content/en/docs/Extensions/collections.md @@ -12,7 +12,7 @@ description: > **OpenSubsonic extension name**: `collections` (As returned by [`getOpenSubsonicExtensions`](../../endpoints/getopensubsonicextensions)) This extension allows users to create and manage collections. -A collection is an ordered list of items - songs, artist, albums, playlists. +A collection is an ordered list of items - songs, artist, albums, playlists, etc. ## Version 1 diff --git a/content/en/docs/Payloads/CollectionItemID.md b/content/en/docs/Payloads/CollectionItemID.md index 36d216fe..a5ca0e9d 100644 --- a/content/en/docs/Payloads/CollectionItemID.md +++ b/content/en/docs/Payloads/CollectionItemID.md @@ -9,6 +9,10 @@ description: > An identifier for a collection item to be added. --- +Depending on the item type, either `id` or `name` must be provided. +For most types, `id` is the natural choice, but some types (e.g. [Genre](../responses/genre)) +don't have it, so `name` should be used. + {{< tabpane persist=false >}} {{< tab header="**Example**:" disabled=true />}} {{< tab header="Song" lang="json">}} @@ -23,9 +27,16 @@ description: > "id": "30" } {{< /tab >}} +{{< tab header="Genre" lang="json">}} +{ + "type": "genre", + "name": "vaporwave" +} +{{< /tab >}} {{< /tabpane >}} | Field | Type | Req. | OpenS. | Details | | --- | --- | --- | --- | --- | -| `type` | `string` | **Yes** | | One of: song, album, artist, playlist | -| `id` | `string` | **Yes** | | An identifier of the item | +| `type` | `string` | **Yes** | | One of: song, album, artist, playlist, genre, internetRadioStation, podcastEpisode, podcast. | +| `id` | `string` | No | | ID of the item, where applicable | +| `name` | `string` | No | | Name of the item, where applicable | diff --git a/content/en/docs/Responses/collectionItem.md b/content/en/docs/Responses/collectionItem.md index dc0f614c..ba15f626 100644 --- a/content/en/docs/Responses/collectionItem.md +++ b/content/en/docs/Responses/collectionItem.md @@ -15,6 +15,10 @@ An item in a collection is a wrapper around one of the following types: - [AlbumID3](../albumid3/) - [ArtistID3](../artistid3/) - [Playlist](../playlist/) +- [Genre](../genre) +- [InternetRadioStation](../internetradiostation) +- [PodcastEpisode](../podcastepisode) +- [PodcastChannel](../podcastchannel) {{< tabpane persist=false >}} {{< tab header="**Example**:" disabled=true />}} @@ -265,4 +269,64 @@ An item in a collection is a wrapper around one of the following types: } } {{< /tab >}} +{{< tab header="Genre" lang="json">}} +{ + "genre": { + "value": "vaporwave", + "songCount": 6, + "albumCount": 1 + } +} +{{< /tab >}} +{{< tab header="Internet radio station" lang="json">}} +{ + "internetRadioStation": { + "id": "2", + "name": "HBR1.com - I.D.M. Tranceponder", + "streamUrl": "http://ubuntu.hbr1.com:19800/trance.ogg", + "homePageUrl": "http://www.hbr1.com/" + } +} +{{< /tab >}} +{{< tab header="Podcast episode" lang="json">}} +{ + "podcastEpisode": { + "id": "7390", + "parent": "7389", + "isDir": "false", + "title": "Jonas Gahr Støre", + "album": "NRK – Hallo P3", + "artist": "Podcast", + "year": "2015", + "coverArt": "7389", + "size": "41808585", + "contentType": "audio/mpeg", + "suffix": "mp3", + "duration": "2619", + "bitRate": "128", + "isVideo": "false", + "created": "2015-09-07T20:07:31.000Z", + "artistId": "453", + "type": "podcast", + "streamId": "7410", + "channelId": "17", + "description": "Jonas Gahr Støre fra Arbeiderpartiet er med i dagens partilederutspørring i Hallo P3!", + "status": "completed", + "publishDate": "2015-09-07T15:29:00.000Z" + } +} +{{< /tab >}} +{{< tab header="Podcast" lang="json">}} +{ + "podcast": { + "id": "1", + "url": "http://downloads.bbc.co.uk/podcasts/fivelive/drkarl/rss.xml", + "title": "Dr Karl and the Naked Scientist", + "description": "Dr Chris Smith aka The Naked Scientist with the latest news from the world of science and Dr Karl answers listeners' science questions.", + "coverArt": "pod-1", + "originalImageUrl": "http://downloads.bbc.co.uk/podcasts/fivelive/drkarl/drkarl.jpg", + "status": "completed" + } +} +{{< /tab >}} {{< /tabpane >}} diff --git a/content/en/docs/Responses/collectionWithItems.md b/content/en/docs/Responses/collectionWithItems.md index 00385a1d..fa3fefea 100644 --- a/content/en/docs/Responses/collectionWithItems.md +++ b/content/en/docs/Responses/collectionWithItems.md @@ -71,6 +71,13 @@ description: > "year": 2005, "genre": "Hip-Hop" } + }, + { + "genre": { + "value": "vaporwave", + "songCount": 6, + "albumCount": 1 + } } ] } diff --git a/openapi/endpoints/createCollection.json b/openapi/endpoints/createCollection.json index 821155ba..8f683dec 100644 --- a/openapi/endpoints/createCollection.json +++ b/openapi/endpoints/createCollection.json @@ -31,7 +31,7 @@ "items": { "$ref": "../schemas/CollectionItemId.json" }, - "description": "IDs of items in the collection." + "description": "Identifiers of items in the collection." } }, "required": [ diff --git a/openapi/schemas/CollectionItem.json b/openapi/schemas/CollectionItem.json index 2e3afc44..6239234d 100644 --- a/openapi/schemas/CollectionItem.json +++ b/openapi/schemas/CollectionItem.json @@ -33,6 +33,38 @@ "$ref": "Playlist.json" } } + }, + { + "type": "object", + "properties": { + "genre": { + "$ref": "Genre.json" + } + } + }, + { + "type": "object", + "properties": { + "internetRadioStation": { + "$ref": "InternetRadioStation.json" + } + } + }, + { + "type": "object", + "properties": { + "podcastEpisode": { + "$ref": "PodcastEpisode.json" + } + } + }, + { + "type": "object", + "properties": { + "podcast": { + "$ref": "PodcastChannel.json" + } + } } ] } diff --git a/openapi/schemas/CollectionItemId.json b/openapi/schemas/CollectionItemId.json index aa004720..8911d7b6 100644 --- a/openapi/schemas/CollectionItemId.json +++ b/openapi/schemas/CollectionItemId.json @@ -1,28 +1,59 @@ { - "type": "object", - "description": "Collection item identifier.", - "properties": { - "type": { - "type": "string", - "description": "Type of the collection item.", - "enum": [ - "song", - "album", - "artist", - "playlist" - ] + "allOf": [ + { + "type": "object", + "description": "Collection item identifier. Depending on the item type, either `id` or `name` must be provided.", + "properties": { + "type": { + "type": "string", + "description": "Type of the collection item.", + "enum": [ + "song", + "album", + "artist", + "playlist", + "genre", + "internetRadioStation", + "podcastEpisode", + "podcast" + ] + } + }, + "required": [ + "type" + ], + "externalDocs": { + "description": "CollectionItemId", + "url": "https://opensubsonic.netlify.app/docs/payloads/collectionitemid/" + } }, - "id": { - "type": "string", - "description": "Id of the item." + { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID of the item, where applicable." + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the item, where applicable." + } + }, + "required": [ + "name" + ] + } + ] } - }, - "required": [ - "type", - "id" - ], - "externalDocs": { - "description": "CollectionItemId", - "url": "https://opensubsonic.netlify.app/docs/payloads/collectionitemid/" - } + ] } From df8066dd31a3565ca9e65dede1ff6c0b676ea580 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Tue, 2 Jun 2026 19:05:30 +0200 Subject: [PATCH 04/14] collections: add coverArt field --- content/en/docs/Responses/collection.md | 1 + openapi/schemas/Collection.json | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/content/en/docs/Responses/collection.md b/content/en/docs/Responses/collection.md index 440e95c8..4f04c3f0 100644 --- a/content/en/docs/Responses/collection.md +++ b/content/en/docs/Responses/collection.md @@ -38,6 +38,7 @@ Does not contain the items in the collection, look at [collectionWithItems](../. | `itemCount` | `int` | **Yes** | | number of items | | `created` | `string` | **Yes** | | Creation date [ISO 8601] | | `changed` | `string` | **Yes** | | Last changed date [ISO 8601] | +| `coverArt` | `string` | No | | A cover Art Id | | `allowedUser` | Array of `string` | No | | A list of allowed usernames | | `readonly` | `boolean` | No | | If true the collection cannot be edited by the current user | | `validUntil` | `string` | No | | Date the collection contents are considered valid until [ISO 8601] | diff --git a/openapi/schemas/Collection.json b/openapi/schemas/Collection.json index 589c7656..20195b9d 100644 --- a/openapi/schemas/Collection.json +++ b/openapi/schemas/Collection.json @@ -36,6 +36,10 @@ "format": "date-time", "description": "Last changed date [ISO 8601]" }, + "coverArt": { + "type": "string", + "description": "A cover Art Id" + }, "allowedUser": { "type": "array", "items": { From 6593dac36208ac84e79bf35a7f5b8ac406baa8de Mon Sep 17 00:00:00 2001 From: Yuriy Date: Tue, 2 Jun 2026 19:15:07 +0200 Subject: [PATCH 05/14] getCollection: add itemCount and itemOffset params --- content/en/docs/Endpoints/getcollection.md | 2 ++ openapi/endpoints/getCollection.json | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/content/en/docs/Endpoints/getcollection.md b/content/en/docs/Endpoints/getcollection.md index be62a031..09edcd19 100644 --- a/content/en/docs/Endpoints/getcollection.md +++ b/content/en/docs/Endpoints/getcollection.md @@ -18,6 +18,8 @@ Returns a collection with the items it contains. | Parameter | Req. | OpenS. | Default | Comment | | --- | --- | --- | --- | --- | | `id` | **Yes** | | | ID of the collection to return, as obtained by `getCollections`. | +| `itemCount` | No | | | The number of items from the collection to return. If unset or set to a negative value, return all items. | +| `itemOffset` | No | | | The number of items to skip. | ### Example diff --git a/openapi/endpoints/getCollection.json b/openapi/endpoints/getCollection.json index 580614c9..5bef4691 100644 --- a/openapi/endpoints/getCollection.json +++ b/openapi/endpoints/getCollection.json @@ -16,6 +16,28 @@ "schema": { "type": "string" } + }, + { + "name": "itemCount", + "in": "query", + "description": "The number of items from the collection to return. If unset or set to a negative value, return all items.", + "required": false, + "schema": { + "type": "integer", + "default": -1, + "minimum": 0 + } + }, + { + "name": "itemOffset", + "in": "query", + "description": "The number of items to skip.", + "required": false, + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + } } ], "responses": { From 97ec0b493bf77184f2f860e0a7c26dd83dd0a9fd Mon Sep 17 00:00:00 2001 From: Yuriy Date: Fri, 5 Jun 2026 09:16:12 +0200 Subject: [PATCH 06/14] collections: extract CreateCollectionRequest schema --- content/en/docs/Endpoints/createcollection.md | 19 +++------ .../docs/Payloads/CreateCollectionRequest.md | 41 +++++++++++++++++++ openapi/endpoints/createCollection.json | 26 +----------- .../CreateCollectionRequest.json | 28 +++++++++++++ openapi/openapi.json | 3 ++ 5 files changed, 79 insertions(+), 38 deletions(-) create mode 100644 content/en/docs/Payloads/CreateCollectionRequest.md create mode 100644 openapi/endpoints/createCollection/CreateCollectionRequest.json diff --git a/content/en/docs/Endpoints/createcollection.md b/content/en/docs/Endpoints/createcollection.md index 41c2eb62..b3cc4a73 100644 --- a/content/en/docs/Endpoints/createcollection.md +++ b/content/en/docs/Endpoints/createcollection.md @@ -16,21 +16,10 @@ This endpoint must be accessed using an HTTP POST request. ### Request Body -The request payload should be provided in the body as a JSON object. - -| Parameter | Req. | OpenS. | Default | Comment | -| --- | --- | --- | --- | --- | -| `name` | **Yes** | | | The human-readable name of the collection. | -| `comment` | No | | | The collection comment. | -| `public` | No | | `false` | `true` if the collection should be visible to all users, `false` otherwise. | -| `items` | No | | | A list of [collectionItemID](../payloads/collectionitemid) objects. | - -### Example request - -{{< alert color="primary" >}} `POST http://your-server/rest/createCollection.view?u=demo&p=demo&v=1.16.1&c=AwesomeClientName&f=json` {{< /alert >}} +The request payload should be provided in the body as a JSON object, as specified by the [CreateCollectionRequest](../payloads/createcollectionrequest) schema. {{< tabpane persist=false >}} -{{< tab header="Request body" lang="json">}} +{{< tab header="CreateCollectionRequest" lang="json">}} { "name": "test collection", "comment": "this is a collection", @@ -52,6 +41,10 @@ The request payload should be provided in the body as a JSON object. {{< /tab >}} {{< /tabpane >}} +### Example request + +{{< alert color="primary" >}} `POST http://your-server/rest/createCollection.view?u=demo&p=demo&v=1.16.1&c=AwesomeClientName&f=json` {{< /alert >}} + ### Result A [`subsonic-response`](../../responses/subsonic-response) element with a top-level [`collection`](../../responses/collection) object on success. diff --git a/content/en/docs/Payloads/CreateCollectionRequest.md b/content/en/docs/Payloads/CreateCollectionRequest.md new file mode 100644 index 00000000..9bbac694 --- /dev/null +++ b/content/en/docs/Payloads/CreateCollectionRequest.md @@ -0,0 +1,41 @@ +--- +title: "CreateCollectionRequest" +linkTitle: "CreateCollectionRequest [OS]" +categories: +- Collections +opensubsonic: +- Extension +description: > + Request payload for creating a collection. +--- + +{{< tabpane persist=false >}} +{{< tab header="**Example**:" disabled=true />}} +{{< tab header="OpenSubsonic" lang="json">}} +{ + "name": "test collection", + "comment": "this is a collection", + "items": [ + { + "type": "song", + "id": "300000060" + }, + { + "type": "album", + "id": "200000021" + }, + { + "type": "genre", + "name": "vaporwave" + } + ] +} +{{< /tab >}} +{{< /tabpane >}} + +| Field | Type | Req. | OpenS. | Details | +| --- | --- | --- | --- | --- | +| `name` | string | **Yes** | **Yes** | The human-readable name of the collection. | +| `comment` | string | No | **Yes** | The collection comment. | +| `public` | boolean | No | **Yes** | `true` if the collection should be visible to all users, `false` otherwise. Default `false`. | +| `items` | [CollectionItemID[]](../payloads/collectionitemid) | No | **Yes** | A list of items. | diff --git a/openapi/endpoints/createCollection.json b/openapi/endpoints/createCollection.json index 8f683dec..8e14c5e2 100644 --- a/openapi/endpoints/createCollection.json +++ b/openapi/endpoints/createCollection.json @@ -12,31 +12,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The human-readable name of the collection." - }, - "comment": { - "type": "string", - "description": "The collection comment." - }, - "public": { - "description": "`true` if the collection should be visible to all users, `false` otherwise.", - "type": "boolean" - }, - "items": { - "type": "array", - "items": { - "$ref": "../schemas/CollectionItemId.json" - }, - "description": "Identifiers of items in the collection." - } - }, - "required": [ - "name" - ] + "$ref": "./createCollection/CreateCollectionRequest.json" } } } diff --git a/openapi/endpoints/createCollection/CreateCollectionRequest.json b/openapi/endpoints/createCollection/CreateCollectionRequest.json new file mode 100644 index 00000000..6618a963 --- /dev/null +++ b/openapi/endpoints/createCollection/CreateCollectionRequest.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The human-readable name of the collection." + }, + "comment": { + "type": "string", + "description": "The collection comment." + }, + "public": { + "description": "`true` if the collection should be visible to all users, `false` otherwise. Default is `false`.", + "default": "false", + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "../../schemas/CollectionItemId.json" + }, + "description": "Identifiers of items in the collection." + } + }, + "required": [ + "name" + ] +} diff --git a/openapi/openapi.json b/openapi/openapi.json index 9d5948e4..8fa0d3af 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -509,6 +509,9 @@ "CollectionWithItems": { "$ref": "./schemas/CollectionWithItems.json" }, + "CreateCollectionRequest": { + "$ref": "./endpoints/createCollection/CreateCollectionRequest.json" + }, "CreateCollectionSuccessResponse": { "$ref": "./endpoints/createCollection/CreateCollectionSuccessResponse.json" }, From 1ff6339f710bc4aa2439b80d9aa65ce543e14590 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Fri, 5 Jun 2026 12:43:30 +0200 Subject: [PATCH 07/14] collections: extract UpdateCollectionRequest schema --- content/en/docs/Endpoints/updatecollection.md | 24 ++----- .../docs/Payloads/CreateCollectionRequest.md | 2 + content/en/docs/Payloads/MoveRange.md | 27 ++++++++ .../docs/Payloads/UpdateCollectionRequest.md | 63 +++++++++++++++++++ openapi/endpoints/updateCollection.json | 52 ++------------- .../UpdateCollectionRequest.json | 42 +++++++++++++ openapi/openapi.json | 6 ++ openapi/schemas/MoveRange.json | 10 +++ 8 files changed, 161 insertions(+), 65 deletions(-) create mode 100644 content/en/docs/Payloads/MoveRange.md create mode 100644 content/en/docs/Payloads/UpdateCollectionRequest.md create mode 100644 openapi/endpoints/updateCollection/UpdateCollectionRequest.json create mode 100644 openapi/schemas/MoveRange.json diff --git a/content/en/docs/Endpoints/updatecollection.md b/content/en/docs/Endpoints/updatecollection.md index a4203424..67d0484b 100644 --- a/content/en/docs/Endpoints/updatecollection.md +++ b/content/en/docs/Endpoints/updatecollection.md @@ -12,27 +12,11 @@ description: > Updates a collection. Only the owner of a collection is allowed to update it. This endpoint must be accessed using an HTTP POST request. -Only the fields specified in the request payload will have an effect on a collection. ### Request Body -The request payload should be provided in the body as a JSON object. - -| Parameter | Req. | OpenS. | Default | Comment | -| --- | --- | --- | --- | --- | -| `collectionId` | **Yes** | | | The collection ID. | -| `name` | No | | | The human-readable name of the collection. | -| `comment` | No | | | The collection comment. | -| `public` | No | | | `true` if the collection should be visible to all users, `false` otherwise. | -| `add` | No | | | Add the specified items to the collection. The payload is an array of [collectionItemID](../payloads/collectionitemid) objects. Items are appended to the tail of the collection. | -| `move` | No | | | Move items from the original position to a new one in the collection. The original range is specified by the `fromStart` (inclusive) and `fromEnd` (exclusive) positions. The first item in the range moves to the position specified by `to` | -| `remove` | No | | | Remove the items at the specified positions from the collection. The payload is an array of integer indices. | - -Only one of `add`, `move`, and `remove` can be non-empty in a single request. - -### Example request - -{{< alert color="primary" >}} `POST http://your-server/rest/updateCollection.view?u=demo&p=demo&v=1.13.0&c=AwesomeClientName&f=json` {{< /alert >}} +The request payload should be provided in the body as a JSON object, as specified by the [UpdateCollectionRequest](../payloads/updatecollectionrequest) schema. +Only the fields specified in the request payload will have an effect on a collection. {{< tabpane persist=false >}} {{< tab header="**Body**:" disabled=true />}} @@ -72,6 +56,10 @@ Only one of `add`, `move`, and `remove` can be non-empty in a single request. {{< /tab >}} {{< /tabpane >}} +### Example request + +{{< alert color="primary" >}} `POST http://your-server/rest/updateCollection.view?u=demo&p=demo&v=1.13.0&c=AwesomeClientName&f=json` {{< /alert >}} + ### Result An empty [`subsonic-response`](../../responses/subsonic-response) element on success. diff --git a/content/en/docs/Payloads/CreateCollectionRequest.md b/content/en/docs/Payloads/CreateCollectionRequest.md index 9bbac694..d513f844 100644 --- a/content/en/docs/Payloads/CreateCollectionRequest.md +++ b/content/en/docs/Payloads/CreateCollectionRequest.md @@ -9,6 +9,8 @@ description: > Request payload for creating a collection. --- +Should be used in the [CreateCollection](../endpoints/createcollection) endpoint. + {{< tabpane persist=false >}} {{< tab header="**Example**:" disabled=true />}} {{< tab header="OpenSubsonic" lang="json">}} diff --git a/content/en/docs/Payloads/MoveRange.md b/content/en/docs/Payloads/MoveRange.md new file mode 100644 index 00000000..43e00bb8 --- /dev/null +++ b/content/en/docs/Payloads/MoveRange.md @@ -0,0 +1,27 @@ +--- +title: "MoveRange" +linkTitle: "MoveRange [OS]" +categories: +- Collections +opensubsonic: +- Extension +description: > + Move a range of items from one position to another. +--- + +{{< tabpane persist=false >}} +{{< tab header="**Example**:" disabled=true />}} +{{< tab header="OpenSubsonic" lang="json">}} +{ + "fromStart": 0, + "fromEnd": 5, + "to": 20 +} +{{< /tab >}} +{{< /tabpane >}} + +| Field | Type | Req. | OpenS. | Details | +| --- | --- | --- | --- | --- | +| `fromStart` | integer | **Yes** | **Yes** | Start of the original range (inclusive). | +| `fromEnd` | integer | **Yes** | **Yes** | End of the original range (exclusive). | +| `to` | integer | **Yes** | **Yes** | New index of the first item in the range. | diff --git a/content/en/docs/Payloads/UpdateCollectionRequest.md b/content/en/docs/Payloads/UpdateCollectionRequest.md new file mode 100644 index 00000000..a1e80fdc --- /dev/null +++ b/content/en/docs/Payloads/UpdateCollectionRequest.md @@ -0,0 +1,63 @@ +--- +title: "UpdateCollectionRequest" +linkTitle: "UpdateCollectionRequest [OS]" +categories: +- Collections +opensubsonic: +- Extension +description: > + Request payload for updating a collection. +--- + +Should be used in the [UpdateCollection](../endpoints/updatecollection) endpoint. +Only the fields supplied in the request payload will have an effect on a collection. + +{{< tabpane persist=false >}} +{{< tab header="**Example**:" disabled=true />}} +{{< tab header="Name and comment" lang="json">}} +{ + "collectionId": "23785995", + "name": "new name", + "comment": "this is a new comment" +} +{{< /tab >}} +{{< tab header="Add" lang="json">}} +{ + "collectionId": "23785995", + "add": [ + { + "type": "album", + "id": "1234" + } + ] +} +{{< /tab >}} +{{< tab header="Move" lang="json">}} +{ + "collectionId": "23785995", + "move": { + "fromStart": 0, + "fromEnd": 5, + "to": 20 + } +} +{{< /tab >}} +{{< tab header="Remove" lang="json">}} +{ + "collectionId": "23785995", + "remove": [0, 1, 2, 6] +} +{{< /tab >}} +{{< /tabpane >}} + +| Field | Type | Req. | OpenS. | Details | +| --- | --- | --- | --- | --- | +| `collectionId` | string | **Yes** | **Yes** | The collection ID. | +| `name` | string | No | **Yes** | The human-readable name of the collection. Cannot be an empty string. | +| `comment` | string | No | **Yes** | The collection comment. | +| `public` | boolean | No | **Yes** | `true` if the collection should be visible to all users, `false` otherwise. | +| `add` | [CollectionItemID[]](../payloads/collectionitemid) | No | **Yes** | Add the specified items to the collection. Items are appended to the tail of the collection. | +| `move` | [MoveRange](../payloads/moverange) | No | **Yes** | Move items from the original position to another in the collection. | +| `remove` | Array of integer | No | **Yes** | Remove the items at the specified positions from the collection. | + +Only one of `add`, `move`, and `remove` can be non-empty in a single request. diff --git a/openapi/endpoints/updateCollection.json b/openapi/endpoints/updateCollection.json index 422342f5..6ae9caea 100644 --- a/openapi/endpoints/updateCollection.json +++ b/openapi/endpoints/updateCollection.json @@ -1,7 +1,7 @@ { "post": { "summary": "Updates a collection.", - "description": "Updates a collection. Only the owner of a collection is allowed to update it. Only the fields specified in the request payload will have an effect on a collection.", + "description": "Updates a collection. Only the owner of a collection is allowed to update it.", "operationId": "updateCollection", "tags": [ "Collections", @@ -12,52 +12,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "collectionId": { - "description": "The collection ID.", - "type": "string" - }, - "name": { - "description": "The human-readable name of the collection.", - "type": "string" - }, - "comment": { - "description": "The collection comment.", - "type": "string" - }, - "public": { - "description": "`true` if the collection should be visible to all users, `false` otherwise.", - "type": "boolean" - }, - "add": { - "description": "Add the specified items to the collection. Multiple items can be specified.", - "type": "array", - "items": { - "$ref": "../schemas/CollectionItemId.json" - } - }, - "move": { - "description": "Move items from the original position to a new one in the collection. The original range is specified by the `fromStart` (inclusive) and `fromEnd` (exclusive) positions. The first item in the range moves to the position specified by `to`", - "type": "object", - "required": ["fromStart", "fromEnd", "to"], - "properties": { - "fromStart": {"type": "integer"}, - "fromEnd": {"type": "integer"}, - "to": {"type": "integer"} - } - }, - "remove": { - "description": "Remove the item at this position from the collection. Multiple items can be specified.", - "type": "array", - "items": { - "type": "integer" - } - } - }, - "required": [ - "collectionId" - ] + "$ref": "./updateCollection/UpdateCollectionRequest.json" } } } @@ -67,6 +22,9 @@ "200": { "$ref": "../responses/EmptySubsonicResponse.json" }, + "400": { + "descritpion": "Request is malformed or doesn't follow the schema." + }, "404": { "description": "Collection does not exist, or extension not supported." }, diff --git a/openapi/endpoints/updateCollection/UpdateCollectionRequest.json b/openapi/endpoints/updateCollection/UpdateCollectionRequest.json new file mode 100644 index 00000000..ae4e05bf --- /dev/null +++ b/openapi/endpoints/updateCollection/UpdateCollectionRequest.json @@ -0,0 +1,42 @@ +{ + "type": "object", + "description": "Request payload for updating a collection. Only the fields supplied in the request payload will have an effect. Only one of `add`, `move`, and `remove` can be non-empty in a single request.", + "properties": { + "collectionId": { + "description": "The collection ID.", + "type": "string" + }, + "name": { + "description": "The human-readable name of the collection.", + "type": "string" + }, + "comment": { + "description": "The collection comment.", + "type": "string" + }, + "public": { + "description": "`true` if the collection should be visible to all users, `false` otherwise.", + "type": "boolean" + }, + "add": { + "description": "Add the specified items to the collection. Multiple items can be specified.", + "type": "array", + "items": { + "$ref": "../../schemas/CollectionItemId.json" + } + }, + "move": { + "$ref": "../../schemas/MoveRange.json" + }, + "remove": { + "description": "Remove the item at this position from the collection. Multiple items can be specified.", + "type": "array", + "items": { + "type": "integer" + } + } + }, + "required": [ + "collectionId" + ] +} diff --git a/openapi/openapi.json b/openapi/openapi.json index 8fa0d3af..5a244aef 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -518,6 +518,12 @@ "CreateCollectionResponse": { "$ref": "./endpoints/createCollection/CreateCollectionResponse.json" }, + "UpdateCollectionRequest": { + "$ref": "./endpoints/updateCollection/UpdateCollectionRequest.json" + }, + "MoveRange": { + "$ref": "./schemas/MoveRange.json" + }, "Share": { "$ref": "./schemas/Share.json" }, diff --git a/openapi/schemas/MoveRange.json b/openapi/schemas/MoveRange.json new file mode 100644 index 00000000..30acfd55 --- /dev/null +++ b/openapi/schemas/MoveRange.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "description": "Move a range of items from one position to another. The range is specified by the `fromStart` (inclusive) and `fromEnd` (exclusive) indices. The first item in the range moves to the index `to`.", + "properties": { + "fromStart": {"type": "integer"}, + "fromEnd": {"type": "integer"}, + "to": {"type": "integer"} + }, + "required": ["fromStart", "fromEnd", "to"] +} From 7848329629a8801910538aed6703c900ddca1fb2 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Fri, 5 Jun 2026 13:19:20 +0200 Subject: [PATCH 08/14] getCollections: add pagination --- content/en/docs/Endpoints/getcollections.md | 10 ++++++--- openapi/endpoints/getCollections.json | 22 +++++++++++++++++++ .../GetCollectionsSuccessResponse.json | 9 ++++++-- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/content/en/docs/Endpoints/getcollections.md b/content/en/docs/Endpoints/getcollections.md index 4ba6ed0e..3813b075 100644 --- a/content/en/docs/Endpoints/getcollections.md +++ b/content/en/docs/Endpoints/getcollections.md @@ -18,10 +18,12 @@ Returns all collections a user has access to. | Parameter | Req. | OpenS. | Default | Comment | | --- | --- | --- | --- | --- | | `username` | No | | | If specified, return collections for this user rather than for the authenticated user. The authenticated user must have admin role if this parameter is used. | +| `count` | No | | | The number of collections to return. If unset or set to a negative value, return all collections. | +| `offset` | No | | | The number of collections to skip. | ### Example -{{< alert color="primary" >}} `http://your-server/rest/getCollections.view?u=demo&p=demo&v=1.13.0&c=AwesomeClientName&f=json` {{< /alert >}} +{{< alert color="primary" >}} `http://your-server/rest/getCollections.view?count=2&u=demo&p=demo&v=1.13.0&c=AwesomeClientName&f=json` {{< /alert >}} ### Result @@ -60,7 +62,8 @@ A [`subsonic-response`](../../responses/subsonic-response) element with a top-le "changed": "2026-03-16T03:18:41+00:00", "readonly": true } - ] + ], + "totalCount": 10 } } {{< /tab >}} @@ -68,4 +71,5 @@ A [`subsonic-response`](../../responses/subsonic-response) element with a top-le | Field | Type | Req. | OpenS. | Details | | --- | --- | --- | --- | --- | -| `collections` | Array of [`collection`](../../responses/collection) | **Yes** | | The collections | +| `collections` | Array of [`collection`](../../responses/collection) | **Yes** | | The collections. | +| `totalCount` | integer | **Yes** | | The total number of collections a user has access to. | diff --git a/openapi/endpoints/getCollections.json b/openapi/endpoints/getCollections.json index 8933eea7..75d69e10 100644 --- a/openapi/endpoints/getCollections.json +++ b/openapi/endpoints/getCollections.json @@ -16,6 +16,28 @@ "schema": { "type": "string" } + }, + { + "name": "count", + "in": "query", + "description": "The number of collections to return. If unset or set to a negative value, return all collections.", + "required": false, + "schema": { + "type": "integer", + "default": -1, + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "description": "The number of collections to skip.", + "required": false, + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + } } ], "responses": { diff --git a/openapi/endpoints/getCollections/GetCollectionsSuccessResponse.json b/openapi/endpoints/getCollections/GetCollectionsSuccessResponse.json index a6b9f035..1cc27b1a 100644 --- a/openapi/endpoints/getCollections/GetCollectionsSuccessResponse.json +++ b/openapi/endpoints/getCollections/GetCollectionsSuccessResponse.json @@ -11,11 +11,16 @@ "items": { "$ref": "../../schemas/Collection.json" }, - "description": "The collections" + "description": "The collections." + }, + "totalCount": { + "type": "integer", + "description": "The total number of collections a user has access to." } }, "required": [ - "collections" + "collections", + "totalCount" ] } ] From 4543fff2907dfc43fe73c03cb30f68e3f9955d5b Mon Sep 17 00:00:00 2001 From: Yuriy Date: Fri, 5 Jun 2026 13:45:40 +0200 Subject: [PATCH 09/14] collections: basic error documentation --- content/en/docs/Endpoints/createcollection.md | 1 + content/en/docs/Endpoints/updatecollection.md | 1 + openapi/endpoints/createCollection.json | 5 ++++- openapi/endpoints/updateCollection.json | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/content/en/docs/Endpoints/createcollection.md b/content/en/docs/Endpoints/createcollection.md index b3cc4a73..bbcf99b0 100644 --- a/content/en/docs/Endpoints/createcollection.md +++ b/content/en/docs/Endpoints/createcollection.md @@ -48,6 +48,7 @@ The request payload should be provided in the body as a JSON object, as specifie ### Result A [`subsonic-response`](../../responses/subsonic-response) element with a top-level [`collection`](../../responses/collection) object on success. +In case of an error, a standard HTTP error code is returned with a descriptive message. {{< tabpane persist=false >}} {{< tab header="**Example**:" disabled=true />}} diff --git a/content/en/docs/Endpoints/updatecollection.md b/content/en/docs/Endpoints/updatecollection.md index 67d0484b..699aaff6 100644 --- a/content/en/docs/Endpoints/updatecollection.md +++ b/content/en/docs/Endpoints/updatecollection.md @@ -63,6 +63,7 @@ Only the fields specified in the request payload will have an effect on a collec ### Result An empty [`subsonic-response`](../../responses/subsonic-response) element on success. +In case of an error, a standard HTTP error code is returned with a descriptive message. {{< tabpane persist=false >}} {{< tab header="**Example**:" disabled=true />}} diff --git a/openapi/endpoints/createCollection.json b/openapi/endpoints/createCollection.json index 8e14c5e2..33ba04b3 100644 --- a/openapi/endpoints/createCollection.json +++ b/openapi/endpoints/createCollection.json @@ -29,8 +29,11 @@ } } }, + "400": { + "description": "Request is malformed or doesn't follow the schema." + }, "404": { - "description": "Extension not supported." + "description": "Some items don't exist, or extension not supported." }, "405": { "$ref": "../responses/HTTPFormPostNotSupported.json" diff --git a/openapi/endpoints/updateCollection.json b/openapi/endpoints/updateCollection.json index 6ae9caea..1149f29a 100644 --- a/openapi/endpoints/updateCollection.json +++ b/openapi/endpoints/updateCollection.json @@ -23,7 +23,7 @@ "$ref": "../responses/EmptySubsonicResponse.json" }, "400": { - "descritpion": "Request is malformed or doesn't follow the schema." + "description": "Request is malformed or doesn't follow the schema." }, "404": { "description": "Collection does not exist, or extension not supported." From dfe1f390181eb0b4436c9e5016180b2b8d560696 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Tue, 9 Jun 2026 19:09:54 +0200 Subject: [PATCH 10/14] collections: drop allowedUser and validUntil --- content/en/docs/Endpoints/getcollections.md | 3 +-- content/en/docs/Responses/collection.md | 5 +---- content/en/docs/Responses/collectionItem.md | 3 +-- content/en/docs/Responses/collectionWithItems.md | 5 ----- openapi/schemas/Collection.json | 12 ------------ 5 files changed, 3 insertions(+), 25 deletions(-) diff --git a/content/en/docs/Endpoints/getcollections.md b/content/en/docs/Endpoints/getcollections.md index 3813b075..390f1934 100644 --- a/content/en/docs/Endpoints/getcollections.md +++ b/content/en/docs/Endpoints/getcollections.md @@ -48,8 +48,7 @@ A [`subsonic-response`](../../responses/subsonic-response) element with a top-le "created": "2026-03-16T03:18:41+00:00", "changed": "2026-03-16T03:18:41+00:00", "itemCount": 3, - "readonly": true, - "validUntil": "2026-03-23T03:18:41+00:00" + "readonly": true }, { "id": "800000076", diff --git a/content/en/docs/Responses/collection.md b/content/en/docs/Responses/collection.md index 4f04c3f0..db1d4d13 100644 --- a/content/en/docs/Responses/collection.md +++ b/content/en/docs/Responses/collection.md @@ -22,8 +22,7 @@ Does not contain the items in the collection, look at [collectionWithItems](../. "created": "2026-03-16T03:18:41+00:00", "changed": "2026-03-16T03:18:41+00:00", "itemCount": 3, - "readonly": true, - "validUntil": "2026-03-23T03:18:41+00:00" + "readonly": true } {{< /tab >}} {{< /tabpane >}} @@ -39,9 +38,7 @@ Does not contain the items in the collection, look at [collectionWithItems](../. | `created` | `string` | **Yes** | | Creation date [ISO 8601] | | `changed` | `string` | **Yes** | | Last changed date [ISO 8601] | | `coverArt` | `string` | No | | A cover Art Id | -| `allowedUser` | Array of `string` | No | | A list of allowed usernames | | `readonly` | `boolean` | No | | If true the collection cannot be edited by the current user | -| `validUntil` | `string` | No | | Date the collection contents are considered valid until [ISO 8601] | When `readonly` is true, clients should hide or disable UI actions that modify the collection. The value should reflect the current authenticated user's access level. When omitted, clients should assume the collection is editable (`false`). diff --git a/content/en/docs/Responses/collectionItem.md b/content/en/docs/Responses/collectionItem.md index ba15f626..6f65c296 100644 --- a/content/en/docs/Responses/collectionItem.md +++ b/content/en/docs/Responses/collectionItem.md @@ -264,8 +264,7 @@ An item in a collection is a wrapper around one of the following types: "changed": "2023-03-16T03:18:41+00:00", "songCount": 1, "duration": 304, - "readonly": true, - "validUntil": "2023-03-23T03:18:41+00:00" + "readonly": true } } {{< /tab >}} diff --git a/content/en/docs/Responses/collectionWithItems.md b/content/en/docs/Responses/collectionWithItems.md index fa3fefea..d9574157 100644 --- a/content/en/docs/Responses/collectionWithItems.md +++ b/content/en/docs/Responses/collectionWithItems.md @@ -21,7 +21,6 @@ description: > "changed": "2026-03-16T03:18:41+00:00", "itemCount": 3, "readonly": true, - "validUntil": "2026-03-23T03:18:41+00:00", "items": [ { "song": { @@ -94,11 +93,7 @@ description: > | `itemCount` | `int` | **Yes** | | number of items | | `created` | `string` | **Yes** | | Creation date [ISO 8601] | | `changed` | `string` | **Yes** | | Last changed date [ISO 8601] | -| `allowedUser` | Array of `string` | No | | A list of allowed usernames | | `readonly` | `boolean` | No | | If true the collection cannot be edited by the current user | -| `validUntil` | `string` | No | | Date the collection contents are considered valid until [ISO 8601] | | `items` | Array of [`collectionItem`](../collectionitem) | **Yes** | | The list of items | When `readonly` is true, clients should hide or disable UI actions that modify the collection. This is useful for server-generated collections like smart collections, recommendations, or curated system lists. The value should reflect the current authenticated user's access level. When omitted, clients should assume the collection is editable (`false`). - -The `validUntil` field indicates how long the collection contents can be treated as fresh, inspired by HTTP caching semantics. Clients may use this to determine when to refresh the collection data. An empty or absent value indicates no caching guarantee; clients should refresh the collection data on each access. diff --git a/openapi/schemas/Collection.json b/openapi/schemas/Collection.json index 20195b9d..397f5959 100644 --- a/openapi/schemas/Collection.json +++ b/openapi/schemas/Collection.json @@ -40,21 +40,9 @@ "type": "string", "description": "A cover Art Id" }, - "allowedUser": { - "type": "array", - "items": { - "type": "string" - }, - "description": "A list of allowed usernames" - }, "readonly": { "type": "boolean", "description": "If true the collection cannot be edited by the current user" - }, - "validUntil": { - "type": "string", - "format": "date-time", - "description": "Date the collection contents are considered valid until [ISO 8601]. Empty or absent means no caching guarantee." } }, "required": [ From edd6e214f373822ed5d0b96a400e542cad63903d Mon Sep 17 00:00:00 2001 From: Yuriy Date: Mon, 27 Jul 2026 19:59:44 +0200 Subject: [PATCH 11/14] collections: simplify CollectionItemID Drop `name` field, `id` should be enough. --- content/en/docs/Endpoints/createcollection.md | 2 +- content/en/docs/Payloads/CollectionItemID.md | 9 +- .../docs/Payloads/CreateCollectionRequest.md | 2 +- openapi/schemas/CollectionItemId.json | 82 ++++++------------- 4 files changed, 32 insertions(+), 63 deletions(-) diff --git a/content/en/docs/Endpoints/createcollection.md b/content/en/docs/Endpoints/createcollection.md index bbcf99b0..0b879ed3 100644 --- a/content/en/docs/Endpoints/createcollection.md +++ b/content/en/docs/Endpoints/createcollection.md @@ -34,7 +34,7 @@ The request payload should be provided in the body as a JSON object, as specifie }, { "type": "genre", - "name": "vaporwave" + "id": "vaporwave" } ] } diff --git a/content/en/docs/Payloads/CollectionItemID.md b/content/en/docs/Payloads/CollectionItemID.md index a5ca0e9d..61a2be49 100644 --- a/content/en/docs/Payloads/CollectionItemID.md +++ b/content/en/docs/Payloads/CollectionItemID.md @@ -9,9 +9,7 @@ description: > An identifier for a collection item to be added. --- -Depending on the item type, either `id` or `name` must be provided. -For most types, `id` is the natural choice, but some types (e.g. [Genre](../responses/genre)) -don't have it, so `name` should be used. +When the 'id' field is not present for an item type (e.g. [Genre](../responses/genre)), use its name or some other natural identifier. {{< tabpane persist=false >}} {{< tab header="**Example**:" disabled=true />}} @@ -30,7 +28,7 @@ don't have it, so `name` should be used. {{< tab header="Genre" lang="json">}} { "type": "genre", - "name": "vaporwave" + "id": "vaporwave" } {{< /tab >}} {{< /tabpane >}} @@ -38,5 +36,4 @@ don't have it, so `name` should be used. | Field | Type | Req. | OpenS. | Details | | --- | --- | --- | --- | --- | | `type` | `string` | **Yes** | | One of: song, album, artist, playlist, genre, internetRadioStation, podcastEpisode, podcast. | -| `id` | `string` | No | | ID of the item, where applicable | -| `name` | `string` | No | | Name of the item, where applicable | +| `id` | `string` | **Yes** | | ID (or name, if 'id' is not available) of an item. | diff --git a/content/en/docs/Payloads/CreateCollectionRequest.md b/content/en/docs/Payloads/CreateCollectionRequest.md index d513f844..51e130c6 100644 --- a/content/en/docs/Payloads/CreateCollectionRequest.md +++ b/content/en/docs/Payloads/CreateCollectionRequest.md @@ -28,7 +28,7 @@ Should be used in the [CreateCollection](../endpoints/createcollection) endpoint }, { "type": "genre", - "name": "vaporwave" + "id": "vaporwave" } ] } diff --git a/openapi/schemas/CollectionItemId.json b/openapi/schemas/CollectionItemId.json index 8911d7b6..3553eba5 100644 --- a/openapi/schemas/CollectionItemId.json +++ b/openapi/schemas/CollectionItemId.json @@ -1,59 +1,31 @@ { - "allOf": [ - { - "type": "object", - "description": "Collection item identifier. Depending on the item type, either `id` or `name` must be provided.", - "properties": { - "type": { - "type": "string", - "description": "Type of the collection item.", - "enum": [ - "song", - "album", - "artist", - "playlist", - "genre", - "internetRadioStation", - "podcastEpisode", - "podcast" - ] - } - }, - "required": [ - "type" - ], - "externalDocs": { - "description": "CollectionItemId", - "url": "https://opensubsonic.netlify.app/docs/payloads/collectionitemid/" - } - }, - { - "oneOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID of the item, where applicable." - } - }, - "required": [ - "id" - ] - }, - { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the item, where applicable." - } - }, - "required": [ - "name" - ] - } + "type": "object", + "description": "Collection item identifier.", + "properties": { + "type": { + "type": "string", + "description": "Type of the collection item.", + "enum": [ + "song", + "album", + "artist", + "playlist", + "genre", + "internetRadioStation", + "podcastEpisode", + "podcast" ] + }, + "id": { + "type": "string", + "description": "Identifier of an item. When the 'id' field is not present for an item type (e.g. for a genre), use its name or some other natural identifier." } - ] + }, + "required": [ + "type" + ], + "externalDocs": { + "description": "CollectionItemId", + "url": "https://opensubsonic.netlify.app/docs/payloads/collectionitemid/" + } } From cdbeedceaa35c48f6d697ae9cd28ed53e536a3ec Mon Sep 17 00:00:00 2001 From: Yuriy Date: Mon, 27 Jul 2026 20:27:52 +0200 Subject: [PATCH 12/14] collections: createCollection shouldn't return items --- content/en/docs/Endpoints/createcollection.md | 62 +------------------ .../CreateCollectionSuccessResponse.json | 2 +- 2 files changed, 3 insertions(+), 61 deletions(-) diff --git a/content/en/docs/Endpoints/createcollection.md b/content/en/docs/Endpoints/createcollection.md index 0b879ed3..567b5b24 100644 --- a/content/en/docs/Endpoints/createcollection.md +++ b/content/en/docs/Endpoints/createcollection.md @@ -68,65 +68,7 @@ In case of an error, a standard HTTP error code is returned with a descriptive m "public": false, "itemCount": 3, "created": "2023-03-16T03:18:41+00:00", - "changed": "2023-03-16T03:18:41+00:00", - "items": [ - { - "song": { - "id": "300000060", - "parent": "200000002", - "title": "BrownSmoke", - "isDir": false, - "isVideo": false, - "type": "music", - "albumId": "200000002", - "album": "Colorsmoke EP", - "artistId": "100000002", - "artist": "Synthetic", - "coverArt": "300000060", - "duration": 304, - "bitRate": 20, - "bitDepth": 16, - "samplingRate": 44100, - "channelCount": 2, - "userRating": 5, - "averageRating": 5, - "track": 4, - "year": 2007, - "genre": "Electronic", - "size": 792375, - "discNumber": 1, - "suffix": "wma", - "contentType": "audio/x-ms-wma", - "path": "Synthetic/Synthetic_-_Colorsmoke_EP-20k217-2007/04-Synthetic_-_BrownSmokeYSBM20k22khS.wma" - } - }, - { - "album": { - "id": "200000021", - "parent": "100000036", - "album": "Forget and Remember", - "title": "Forget and Remember", - "name": "Forget and Remember", - "isDir": true, - "coverArt": "al-200000021", - "songCount": 20, - "created": "2021-07-22T02:09:31+00:00", - "duration": 4248, - "playCount": 0, - "artistId": "100000036", - "artist": "Comfort Fit", - "year": 2005, - "genre": "Hip-Hop" - } - }, - { - "genre": { - "value": "vaporwave", - "songCount": 6, - "albumCount": 1 - } - } - ] + "changed": "2023-03-16T03:18:41+00:00" } } } @@ -135,4 +77,4 @@ In case of an error, a standard HTTP error code is returned with a descriptive m | Field | Type | Req. | OpenS. | Details | | --- | --- | --- | --- | --- | -| `collection` | [`CollectionWithItems`](../../responses/collectionwithitems) | **Yes** | | The collection | +| `collection` | [`Collection`](../../responses/collection) | **Yes** | | The collection | diff --git a/openapi/endpoints/createCollection/CreateCollectionSuccessResponse.json b/openapi/endpoints/createCollection/CreateCollectionSuccessResponse.json index f7e164c5..08d97fdd 100644 --- a/openapi/endpoints/createCollection/CreateCollectionSuccessResponse.json +++ b/openapi/endpoints/createCollection/CreateCollectionSuccessResponse.json @@ -7,7 +7,7 @@ "type": "object", "properties": { "collection": { - "$ref": "../../schemas/CollectionWithItems.json" + "$ref": "../../schemas/Collection.json" } }, "required": [ From 25f1d9d26778f7a04e54d9fca311fbbb1a5902a8 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Sat, 5 Sep 2026 13:58:43 +0200 Subject: [PATCH 13/14] getCollections: add filter by name --- content/en/docs/Endpoints/getcollections.md | 5 +++-- openapi/endpoints/getCollections.json | 13 +++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/content/en/docs/Endpoints/getcollections.md b/content/en/docs/Endpoints/getcollections.md index 390f1934..0d8cc3d4 100644 --- a/content/en/docs/Endpoints/getcollections.md +++ b/content/en/docs/Endpoints/getcollections.md @@ -6,18 +6,19 @@ OpenSubsonic: categories: - Collections description: > - Returns all collections a user has access to. + Returns collections a user has access to. --- `http://your-server/rest/getCollections` -Returns all collections a user has access to. +Returns collections a user has access to, with optional filters and pagination. ### Parameters | Parameter | Req. | OpenS. | Default | Comment | | --- | --- | --- | --- | --- | | `username` | No | | | If specified, return collections for this user rather than for the authenticated user. The authenticated user must have admin role if this parameter is used. | +| `name` | No | | | If specified, return collections that have the provided string in their name. | | `count` | No | | | The number of collections to return. If unset or set to a negative value, return all collections. | | `offset` | No | | | The number of collections to skip. | diff --git a/openapi/endpoints/getCollections.json b/openapi/endpoints/getCollections.json index 75d69e10..4b36b853 100644 --- a/openapi/endpoints/getCollections.json +++ b/openapi/endpoints/getCollections.json @@ -1,7 +1,7 @@ { "get": { - "summary": "Get all collections a user has access to.", - "description": "Get all collections a user has access to.", + "summary": "Get collections a user has access to.", + "description": "Get collections a user has access to, with optional filters and pagination.", "operationId": "getCollections", "tags": [ "Collections", @@ -17,6 +17,15 @@ "type": "string" } }, + { + "name": "name", + "in": "query", + "description": "If specified, return collections that have the provided string in their name.", + "required": false, + "schema": { + "type": "string" + } + }, { "name": "count", "in": "query", From af0d888c8dce4d09f1affc5172bfb229a22bc7d5 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Sat, 5 Sep 2026 14:13:10 +0200 Subject: [PATCH 14/14] getCollections: add sorting --- content/en/docs/Endpoints/getcollections.md | 2 ++ openapi/endpoints/getCollections.json | 27 +++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/content/en/docs/Endpoints/getcollections.md b/content/en/docs/Endpoints/getcollections.md index 0d8cc3d4..053ee6b4 100644 --- a/content/en/docs/Endpoints/getcollections.md +++ b/content/en/docs/Endpoints/getcollections.md @@ -19,6 +19,8 @@ Returns collections a user has access to, with optional filters and pagination. | --- | --- | --- | --- | --- | | `username` | No | | | If specified, return collections for this user rather than for the authenticated user. The authenticated user must have admin role if this parameter is used. | | `name` | No | | | If specified, return collections that have the provided string in their name. | +| `sort` | No | | name | If specified, sort the list by the provided field or randomly. Sorting is applied before the `count` and `offset` parameters are taken into account. The default is to sort by name. Allowed values: name, created, random. | +| `order` | No | | asc | The order of sorting. Allowed values: asc, desc. | | `count` | No | | | The number of collections to return. If unset or set to a negative value, return all collections. | | `offset` | No | | | The number of collections to skip. | diff --git a/openapi/endpoints/getCollections.json b/openapi/endpoints/getCollections.json index 4b36b853..fd776960 100644 --- a/openapi/endpoints/getCollections.json +++ b/openapi/endpoints/getCollections.json @@ -26,6 +26,33 @@ "type": "string" } }, + { + "name": "sort", + "in": "query", + "description": "If specified, sort the list by the provided field or randomly. Sorting is applied before the `count` and `offset` parameters are taken into account. The default is to sort by name.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "name", + "created", + "random" + ] + } + }, + { + "name": "order", + "in": "query", + "description": "The order of sorting.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, { "name": "count", "in": "query",