feat(api): playlist add-items + search all-items endpoints (#208 #209)

- POST /api/playlists/{id:int}/items wraps the existing AddItemsToPlaylist
  command (mirrors CollectionController.AddItems); controller pre-checks
  playlist existence for a real 404, and the handler now rejects adds to
  system (generated) playlists, matching the guard already applied to
  rename/delete/replace-items so the Blazor path gets the same protection.
- GET /api/search/all-items wraps the existing QuerySearchIndexAllItems
  query, returning a new SearchResultAllItemsResponseModel (never expose
  the VM directly) so the SPA's shared "add all to collection/playlist"
  component can materialize ids before calling the add endpoints, same
  two-step flow Blazor's Search.razor already uses.
- Show-detail DTO check: ShowDetailResponseModel already exposes
  libraryId, title, and mediaSourceKind (serialized as a string enum via
  the global StringEnumConverter) - no changes needed.

Adds controller tests (route table + per-action) for both endpoints and
regenerates the OpenAPI document, endpoint index, and SPA client types.
This commit is contained in:
2026-07-09 23:56:50 +02:00
parent cfabd6f33f
commit 819e751cab
10 changed files with 646 additions and 6 deletions
+384
View File
@@ -6852,6 +6852,94 @@
}
}
}
},
"post": {
"tags": [
"Playlists"
],
"summary": "Add items to a playlist",
"operationId": "AddItemsToPlaylist",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"requestBody": {
"content": {
"application/json-patch+json": {
"schema": {
"$ref": "#/components/schemas/AddItemsToPlaylistRequest"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/AddItemsToPlaylistRequest"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/AddItemsToPlaylistRequest"
}
},
"application/*+json": {
"schema": {
"$ref": "#/components/schemas/AddItemsToPlaylistRequest"
}
}
},
"required": true
},
"responses": {
"204": {
"description": "No Content"
},
"404": {
"description": "Not Found",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"422": {
"description": "Unprocessable Entity",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"/api/playlists/preview": {
@@ -9838,6 +9926,68 @@
}
}
},
"/api/search/all-items": {
"get": {
"tags": [
"Search"
],
"summary": "Search library items across all media kinds and return raw id lists",
"description": "Returns every matching item's id, grouped by media kind, with no paging. Used by the SPA's \"add all to collection/playlist\" flow to materialize ids before calling the add endpoints.",
"operationId": "SearchAllItems",
"parameters": [
{
"name": "query",
"in": "query",
"schema": {
"type": "string",
"default": ""
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/SearchResultAllItemsResponseModel"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchResultAllItemsResponseModel"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/SearchResultAllItemsResponseModel"
}
}
}
},
"422": {
"description": "Unprocessable Entity",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"/api/search/collections": {
"get": {
"tags": [
@@ -13845,6 +13995,123 @@
}
}
},
"AddItemsToPlaylistRequest": {
"required": [
"movieIds",
"showIds",
"seasonIds",
"episodeIds",
"artistIds",
"musicVideoIds",
"otherVideoIds",
"songIds",
"imageIds",
"remoteStreamIds"
],
"type": "object",
"properties": {
"movieIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"showIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"seasonIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"episodeIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"artistIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"musicVideoIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"otherVideoIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"songIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"imageIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"remoteStreamIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
}
}
},
"AddTraktListRequest": {
"required": [
"url"
@@ -20819,6 +21086,123 @@
}
}
},
"SearchResultAllItemsResponseModel": {
"required": [
"movieIds",
"showIds",
"seasonIds",
"episodeIds",
"artistIds",
"musicVideoIds",
"otherVideoIds",
"songIds",
"imageIds",
"remoteStreamIds"
],
"type": "object",
"properties": {
"movieIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"showIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"seasonIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"episodeIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"artistIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"musicVideoIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"otherVideoIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"songIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"imageIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
},
"remoteStreamIds": {
"type": [
"null",
"array"
],
"items": {
"type": "integer",
"format": "int32"
}
}
}
},
"SearchResultGroupResponseModel": {
"required": [
"totalCount",