diff --git a/ErsatzTV/wwwroot/openapi/v1.json b/ErsatzTV/wwwroot/openapi/v1.json index 9cdc5a6ca..21666f098 100644 --- a/ErsatzTV/wwwroot/openapi/v1.json +++ b/ErsatzTV/wwwroot/openapi/v1.json @@ -4169,6 +4169,635 @@ } } }, + "/api/media-sources/emby": { + "get": { + "tags": [ + "Emby" + ], + "summary": "Get Emby connection state and discovered servers", + "operationId": "GetEmbyState", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RemoteMediaSourceStateResponseModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoteMediaSourceStateResponseModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RemoteMediaSourceStateResponseModel" + } + } + } + } + } + } + }, + "/api/media-sources/emby/connection": { + "get": { + "tags": [ + "Emby" + ], + "summary": "Get the Emby connection address", + "description": "Never returns the API key — only whether one is currently configured (design #202 secure connection contract). Use the PUT to (re)connect; a blank apiKey there retains the existing key.", + "operationId": "GetEmbyConnection", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RemoteConnectionResponseModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoteConnectionResponseModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RemoteConnectionResponseModel" + } + } + } + } + } + }, + "put": { + "tags": [ + "Emby" + ], + "summary": "Connect, reconnect, or edit the Emby connection", + "description": "A blank/omitted apiKey retains the existing key; a non-blank value sets a new one. The key is required on first connect (no existing secret).", + "operationId": "SaveEmbyConnection", + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/SaveRemoteConnectionRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/SaveRemoteConnectionRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/SaveRemoteConnectionRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/SaveRemoteConnectionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RemoteConnectionResponseModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoteConnectionResponseModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RemoteConnectionResponseModel" + } + } + } + }, + "409": { + "description": "Conflict", + "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/media-sources/emby/disconnect": { + "post": { + "tags": [ + "Emby" + ], + "summary": "Disconnect Emby", + "description": "Purges the Emby connection, discovered servers, and all synced Emby content.", + "operationId": "DisconnectEmby", + "responses": { + "204": { + "description": "No Content" + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/media-sources/emby/{id}/libraries": { + "get": { + "tags": [ + "Emby" + ], + "summary": "Get an Emby source's libraries", + "operationId": "GetEmbyLibraries", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + } + } + }, + "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" + } + } + } + } + } + }, + "put": { + "tags": [ + "Emby" + ], + "summary": "Replace an Emby source's library sync preferences", + "description": "The body must be the complete set of the source's libraries (design #202 §C4a) — a row absent from the request is rejected, not silently ignored. Ids are not stable across a disable, so re-fetch this response rather than the request body.", + "operationId": "ReplaceEmbyLibraryPreferences", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/ReplaceRemoteLibraryPreferencesRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReplaceRemoteLibraryPreferencesRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ReplaceRemoteLibraryPreferencesRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ReplaceRemoteLibraryPreferencesRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + } + } + }, + "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/media-sources/emby/{id}/path-replacements": { + "get": { + "tags": [ + "Emby" + ], + "summary": "Get an Emby source's path replacements", + "operationId": "GetEmbyPathReplacements", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + } + } + }, + "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" + } + } + } + } + } + }, + "put": { + "tags": [ + "Emby" + ], + "summary": "Replace an Emby source's path replacements", + "operationId": "ReplaceEmbyPathReplacements", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/ReplacePathReplacementsRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReplacePathReplacementsRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ReplacePathReplacementsRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ReplacePathReplacementsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + } + } + }, + "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/media-sources/emby/{id}/refresh-libraries": { + "post": { + "tags": [ + "Emby" + ], + "summary": "Refresh an Emby source's libraries", + "operationId": "RefreshEmbyLibraries", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + }, + "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" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, "/api/ffmpeg/profiles": { "get": { "tags": [ @@ -5318,6 +5947,635 @@ } } }, + "/api/media-sources/jellyfin": { + "get": { + "tags": [ + "Jellyfin" + ], + "summary": "Get Jellyfin connection state and discovered servers", + "operationId": "GetJellyfinState", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RemoteMediaSourceStateResponseModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoteMediaSourceStateResponseModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RemoteMediaSourceStateResponseModel" + } + } + } + } + } + } + }, + "/api/media-sources/jellyfin/connection": { + "get": { + "tags": [ + "Jellyfin" + ], + "summary": "Get the Jellyfin connection address", + "description": "Never returns the API key — only whether one is currently configured (design #202 secure connection contract). Use the PUT to (re)connect; a blank apiKey there retains the existing key.", + "operationId": "GetJellyfinConnection", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RemoteConnectionResponseModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoteConnectionResponseModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RemoteConnectionResponseModel" + } + } + } + } + } + }, + "put": { + "tags": [ + "Jellyfin" + ], + "summary": "Connect, reconnect, or edit the Jellyfin connection", + "description": "A blank/omitted apiKey retains the existing key; a non-blank value sets a new one. The key is required on first connect (no existing secret).", + "operationId": "SaveJellyfinConnection", + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/SaveRemoteConnectionRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/SaveRemoteConnectionRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/SaveRemoteConnectionRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/SaveRemoteConnectionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RemoteConnectionResponseModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoteConnectionResponseModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RemoteConnectionResponseModel" + } + } + } + }, + "409": { + "description": "Conflict", + "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/media-sources/jellyfin/disconnect": { + "post": { + "tags": [ + "Jellyfin" + ], + "summary": "Disconnect Jellyfin", + "description": "Purges the Jellyfin connection, discovered servers, and all synced Jellyfin content.", + "operationId": "DisconnectJellyfin", + "responses": { + "204": { + "description": "No Content" + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/media-sources/jellyfin/{id}/libraries": { + "get": { + "tags": [ + "Jellyfin" + ], + "summary": "Get a Jellyfin source's libraries", + "operationId": "GetJellyfinLibraries", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + } + } + }, + "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" + } + } + } + } + } + }, + "put": { + "tags": [ + "Jellyfin" + ], + "summary": "Replace a Jellyfin source's library sync preferences", + "description": "The body must be the complete set of the source's libraries (design #202 §C4a) — a row absent from the request is rejected, not silently ignored. Ids are not stable across a disable, so re-fetch this response rather than the request body.", + "operationId": "ReplaceJellyfinLibraryPreferences", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/ReplaceRemoteLibraryPreferencesRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReplaceRemoteLibraryPreferencesRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ReplaceRemoteLibraryPreferencesRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ReplaceRemoteLibraryPreferencesRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + } + } + }, + "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/media-sources/jellyfin/{id}/path-replacements": { + "get": { + "tags": [ + "Jellyfin" + ], + "summary": "Get a Jellyfin source's path replacements", + "operationId": "GetJellyfinPathReplacements", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + } + } + }, + "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" + } + } + } + } + } + }, + "put": { + "tags": [ + "Jellyfin" + ], + "summary": "Replace a Jellyfin source's path replacements", + "operationId": "ReplaceJellyfinPathReplacements", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/ReplacePathReplacementsRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReplacePathReplacementsRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ReplacePathReplacementsRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ReplacePathReplacementsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + } + } + }, + "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/media-sources/jellyfin/{id}/refresh-libraries": { + "post": { + "tags": [ + "Jellyfin" + ], + "summary": "Refresh a Jellyfin source's libraries", + "operationId": "RefreshJellyfinLibraries", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + }, + "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" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, "/api/languages": { "get": { "tags": [ @@ -5654,6 +6912,534 @@ } } }, + "/api/libraries/local": { + "get": { + "tags": [ + "Libraries" + ], + "summary": "Get all local libraries", + "operationId": "GetLocalLibraries", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LocalLibraryResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LocalLibraryResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LocalLibraryResponseModel" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Libraries" + ], + "summary": "Create a local library", + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/CreateLocalLibraryRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateLocalLibraryRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CreateLocalLibraryRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CreateLocalLibraryRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/LocalLibraryResponseModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocalLibraryResponseModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LocalLibraryResponseModel" + } + } + } + }, + "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/libraries/local/{id}": { + "get": { + "tags": [ + "Libraries" + ], + "summary": "Get a local library by id", + "operationId": "GetLocalLibrary", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/LocalLibraryDetailResponseModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocalLibraryDetailResponseModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LocalLibraryDetailResponseModel" + } + } + } + }, + "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" + } + } + } + } + } + }, + "put": { + "tags": [ + "Libraries" + ], + "summary": "Update a local library", + "description": "Replaces the library's name and full path list. MediaKind is immutable after create and is not part of this request. Paths are identified by normalized path string, not id — a renamed path is a delete-old + add-new (its id changes).", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/UpdateLocalLibraryRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateLocalLibraryRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UpdateLocalLibraryRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateLocalLibraryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/LocalLibraryDetailResponseModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocalLibraryDetailResponseModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LocalLibraryDetailResponseModel" + } + } + } + }, + "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" + } + } + } + }, + "409": { + "description": "Conflict", + "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" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Libraries" + ], + "summary": "Delete a local library", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "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" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/libraries/local/paths/{pathId}/move": { + "post": { + "tags": [ + "Libraries" + ], + "summary": "Move a local library path to another local library", + "description": "Moves a path (and its scanned media) from its current library to a different local library of the same media kind. Blazor's move dialog enforces same-kind/different-library only client-side; this endpoint enforces both server-side so API/MCP clients cannot bypass them.", + "parameters": [ + { + "name": "pathId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/MoveLocalLibraryPathRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MoveLocalLibraryPathRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MoveLocalLibraryPathRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/MoveLocalLibraryPathRequest" + } + } + }, + "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" + } + } + } + }, + "409": { + "description": "Conflict", + "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/libraries/local/path-exists": { + "post": { + "tags": [ + "Libraries" + ], + "summary": "Check whether a filesystem path exists", + "description": "Server-side existence check for the SPA's add-path UX (a browser cannot call Directory.Exists). This is a check-then-act convenience only — Create/Update still validate new paths at save time (design #202 §C2).", + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/LocalPathCheckRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocalPathCheckRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LocalPathCheckRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LocalPathCheckRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/LocalPathCheckResponseModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocalPathCheckResponseModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LocalPathCheckResponseModel" + } + } + } + } + } + } + }, "/api/logs": { "get": { "tags": [ @@ -8912,6 +10698,583 @@ } } }, + "/api/media-sources/plex": { + "get": { + "tags": [ + "Plex" + ], + "summary": "Get Plex connection state", + "description": "Returns whether ErsatzTV is authorized with plex.tv, whether a Plex sign-in/sync lock is held, and the list of discovered Plex servers. Poll this during the pin flow: authorized && !locked = done.", + "operationId": "GetPlexState", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/RemoteMediaSourceStateResponseModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoteMediaSourceStateResponseModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RemoteMediaSourceStateResponseModel" + } + } + } + } + } + } + }, + "/api/media-sources/plex/pin-flow": { + "post": { + "tags": [ + "Plex" + ], + "summary": "Start the Plex sign-in pin flow", + "description": "Acquires the Plex lock and starts the OAuth pin flow, returning the plex.tv authorization URL to open in a new tab. The lock stays held for the background flow; poll GET /api/media-sources/plex until authorized && !locked. Also used to fix credentials for an existing but unauthorized server.", + "operationId": "StartPlexPinFlow", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/PlexPinFlowResponseModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlexPinFlowResponseModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/PlexPinFlowResponseModel" + } + } + } + }, + "409": { + "description": "Conflict", + "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/media-sources/plex/sign-out": { + "post": { + "tags": [ + "Plex" + ], + "summary": "Sign out of Plex", + "description": "Purges all Plex servers, synced Plex content, and stored credentials. Acquires the Plex lock; the handler always releases it (even on failure).", + "operationId": "SignOutOfPlex", + "responses": { + "204": { + "description": "No Content" + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/media-sources/plex/{id}/libraries": { + "get": { + "tags": [ + "Plex" + ], + "summary": "Get a Plex server's libraries", + "operationId": "GetPlexLibraries", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + } + } + }, + "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" + } + } + } + } + } + }, + "put": { + "tags": [ + "Plex" + ], + "summary": "Replace a Plex server's library sync preferences", + "description": "The body is the complete set of the source's libraries with each shouldSyncItems flag. A row absent from the request is left untouched. Every id must belong to this source (no Id=0 rows). Returns the reloaded list — library ids change when sync is disabled, so re-key any client draft off the response. Enabled libraries are queued for sync (locked libraries are skipped).", + "operationId": "ReplacePlexLibraryPreferences", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/ReplaceRemoteLibraryPreferencesRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReplaceRemoteLibraryPreferencesRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ReplaceRemoteLibraryPreferencesRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ReplaceRemoteLibraryPreferencesRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RemoteLibraryResponseModel" + } + } + } + } + }, + "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/media-sources/plex/{id}/path-replacements": { + "get": { + "tags": [ + "Plex" + ], + "summary": "Get a Plex server's path replacements", + "operationId": "GetPlexPathReplacements", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + } + } + }, + "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" + } + } + } + } + } + }, + "put": { + "tags": [ + "Plex" + ], + "summary": "Replace a Plex server's path replacements", + "description": "Replaces the source's path replacements: an existing id updates, id<1 adds, an existing id absent from the body is deleted. Every positive id must belong to this source and every row needs a non-empty RemotePath and LocalPath (else 422, no partial mutation). Returns the reloaded list.", + "operationId": "ReplacePlexPathReplacements", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/ReplacePathReplacementsRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReplacePathReplacementsRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ReplacePathReplacementsRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ReplacePathReplacementsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathReplacementResponseModel" + } + } + } + } + }, + "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/media-sources/plex/{id}/refresh-libraries": { + "post": { + "tags": [ + "Plex" + ], + "summary": "Refresh a Plex server's libraries", + "description": "Queues a synchronization of the server's libraries (fire-and-forget). Returns 409 while a Plex sign-in/sync lock is held; duplicate refreshes are accepted (the scan is idempotent).", + "operationId": "RefreshPlexLibraries", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + }, + "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" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, "/api/rerun-collections": { "get": { "tags": [ @@ -16786,6 +19149,34 @@ } } }, + "CreateLocalLibraryRequest": { + "required": [ + "name", + "mediaKind", + "paths" + ], + "type": "object", + "properties": { + "name": { + "type": [ + "null", + "string" + ] + }, + "mediaKind": { + "$ref": "#/components/schemas/LibraryMediaKind" + }, + "paths": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + } + } + }, "CreateMultiCollectionRequest": { "required": [ "name", @@ -18406,6 +20797,124 @@ } } }, + "LocalLibraryDetailResponseModel": { + "required": [ + "id", + "name", + "mediaKind", + "isLocked", + "mediaItemCount", + "paths" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "mediaKind": { + "$ref": "#/components/schemas/LibraryMediaKind" + }, + "isLocked": { + "type": "boolean" + }, + "mediaItemCount": { + "type": "integer", + "format": "int32" + }, + "paths": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/LocalLibraryPathResponseModel" + } + } + } + }, + "LocalLibraryPathResponseModel": { + "required": [ + "id", + "path", + "mediaItemCount" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "path": { + "type": [ + "null", + "string" + ] + }, + "mediaItemCount": { + "type": "integer", + "format": "int32" + } + } + }, + "LocalLibraryResponseModel": { + "required": [ + "id", + "name", + "mediaKind", + "isLocked" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "mediaKind": { + "$ref": "#/components/schemas/LibraryMediaKind" + }, + "isLocked": { + "type": "boolean" + } + } + }, + "LocalPathCheckRequest": { + "required": [ + "path" + ], + "type": "object", + "properties": { + "path": { + "type": [ + "null", + "string" + ] + } + } + }, + "LocalPathCheckResponseModel": { + "required": [ + "exists" + ], + "type": "object", + "properties": { + "exists": { + "type": "boolean" + } + } + }, "LogEntryResponseModel": { "required": [ "timestamp", @@ -18901,6 +21410,18 @@ ], "type": "string" }, + "MoveLocalLibraryPathRequest": { + "required": [ + "targetLibraryId" + ], + "type": "object", + "properties": { + "targetLibraryId": { + "type": "integer", + "format": "int32" + } + } + }, "MovieDetailResponseModel": { "required": [ "id", @@ -19307,6 +21828,58 @@ } } }, + "PathReplacementItemRequest": { + "required": [ + "id", + "remotePath", + "localPath" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "remotePath": { + "type": [ + "null", + "string" + ] + }, + "localPath": { + "type": [ + "null", + "string" + ] + } + } + }, + "PathReplacementResponseModel": { + "required": [ + "id", + "remotePath", + "localPath" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "remotePath": { + "type": [ + "null", + "string" + ] + }, + "localPath": { + "type": [ + "null", + "string" + ] + } + } + }, "PlaybackOrder": { "enum": [ "None", @@ -20265,6 +22838,20 @@ } } }, + "PlexPinFlowResponseModel": { + "required": [ + "authUrl" + ], + "type": "object", + "properties": { + "authUrl": { + "type": [ + "null", + "string" + ] + } + } + }, "ProblemDetails": { "type": "object", "properties": { @@ -20340,6 +22927,118 @@ } } }, + "RemoteConnectionResponseModel": { + "required": [ + "address", + "hasApiKey" + ], + "type": "object", + "properties": { + "address": { + "type": [ + "null", + "string" + ] + }, + "hasApiKey": { + "type": "boolean" + } + } + }, + "RemoteLibraryPreferenceRequest": { + "required": [ + "id", + "shouldSyncItems" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "shouldSyncItems": { + "type": "boolean" + } + } + }, + "RemoteLibraryResponseModel": { + "required": [ + "id", + "name", + "mediaKind", + "shouldSyncItems" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "mediaKind": { + "$ref": "#/components/schemas/LibraryMediaKind" + }, + "shouldSyncItems": { + "type": "boolean" + } + } + }, + "RemoteMediaSourceItemResponseModel": { + "required": [ + "id", + "name", + "address" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "address": { + "type": [ + "null", + "string" + ] + } + } + }, + "RemoteMediaSourceStateResponseModel": { + "required": [ + "isAuthorized", + "isLocked", + "servers" + ], + "type": "object", + "properties": { + "isAuthorized": { + "type": "boolean" + }, + "isLocked": { + "type": "boolean" + }, + "servers": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/RemoteMediaSourceItemResponseModel" + } + } + } + }, "ReplaceBlockRequest": { "required": [ "name", @@ -20547,6 +23246,23 @@ } } }, + "ReplacePathReplacementsRequest": { + "required": [ + "items" + ], + "type": "object", + "properties": { + "items": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/PathReplacementItemRequest" + } + } + } + }, "ReplacePlaylistRequest": { "required": [ "name", @@ -20605,6 +23321,23 @@ } } }, + "ReplaceRemoteLibraryPreferencesRequest": { + "required": [ + "libraries" + ], + "type": "object", + "properties": { + "libraries": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/RemoteLibraryPreferenceRequest" + } + } + } + }, "ReplaceScheduleItemsRequest": { "required": [ "items" @@ -20752,6 +23485,27 @@ } } }, + "SaveRemoteConnectionRequest": { + "required": [ + "address", + "apiKey" + ], + "type": "object", + "properties": { + "address": { + "type": [ + "null", + "string" + ] + }, + "apiKey": { + "type": [ + "null", + "string" + ] + } + } + }, "ScalingBehavior": { "enum": [ "ScaleAndPad", @@ -22931,6 +25685,49 @@ } } }, + "UpdateLocalLibraryPathRequest": { + "required": [ + "id", + "path" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "path": { + "type": [ + "null", + "string" + ] + } + } + }, + "UpdateLocalLibraryRequest": { + "required": [ + "name", + "paths" + ], + "type": "object", + "properties": { + "name": { + "type": [ + "null", + "string" + ] + }, + "paths": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/UpdateLocalLibraryPathRequest" + } + } + } + }, "UpdateLoggingSettingsRequest": { "required": [ "defaultMinimumLogLevel", @@ -23535,6 +26332,9 @@ { "name": "DecoTemplates" }, + { + "name": "Emby" + }, { "name": "FFmpeg Profiles" }, @@ -23550,6 +26350,9 @@ { "name": "Images" }, + { + "name": "Jellyfin" + }, { "name": "Languages" }, @@ -23580,6 +26383,9 @@ { "name": "Playouts" }, + { + "name": "Plex" + }, { "name": "Rerun Collections" }, diff --git a/docs/endpoint-index.md b/docs/endpoint-index.md index 0b50cc62c..d1b1ad3e5 100644 --- a/docs/endpoint-index.md +++ b/docs/endpoint-index.md @@ -2,7 +2,7 @@ *Generated by `scripts/generate-endpoint-index.py` from `ErsatzTV/wwwroot/openapi/v1.json`. Do not edit by hand -- regenerated by `scripts/update-openapi.sh`.* -133 endpoints, 207 operations. +155 endpoints, 240 operations. ## Artists @@ -109,6 +109,20 @@ | GET | `/api/decos/{id}` | GetDecoById | Get a deco by id | | PUT | `/api/decos/{id}` | | Replace a deco | +## Emby + +| Method | Path | Operation | Summary | +|---|---|---|---| +| GET | `/api/media-sources/emby` | GetEmbyState | Get Emby connection state and discovered servers | +| GET | `/api/media-sources/emby/connection` | GetEmbyConnection | Get the Emby connection address | +| PUT | `/api/media-sources/emby/connection` | SaveEmbyConnection | Connect, reconnect, or edit the Emby connection | +| POST | `/api/media-sources/emby/disconnect` | DisconnectEmby | Disconnect Emby | +| GET | `/api/media-sources/emby/{id}/libraries` | GetEmbyLibraries | Get an Emby source's libraries | +| PUT | `/api/media-sources/emby/{id}/libraries` | ReplaceEmbyLibraryPreferences | Replace an Emby source's library sync preferences | +| GET | `/api/media-sources/emby/{id}/path-replacements` | GetEmbyPathReplacements | Get an Emby source's path replacements | +| PUT | `/api/media-sources/emby/{id}/path-replacements` | ReplaceEmbyPathReplacements | Replace an Emby source's path replacements | +| POST | `/api/media-sources/emby/{id}/refresh-libraries` | RefreshEmbyLibraries | Refresh an Emby source's libraries | + ## FFmpeg Profiles | Method | Path | Operation | Summary | @@ -149,6 +163,20 @@ | GET | `/api/images/folders` | GetImageFolders | List image library folders | | PUT | `/api/images/folders/{id}/duration` | UpdateImageFolderDuration | Set or clear an image folder's playout duration | +## Jellyfin + +| Method | Path | Operation | Summary | +|---|---|---|---| +| GET | `/api/media-sources/jellyfin` | GetJellyfinState | Get Jellyfin connection state and discovered servers | +| GET | `/api/media-sources/jellyfin/connection` | GetJellyfinConnection | Get the Jellyfin connection address | +| PUT | `/api/media-sources/jellyfin/connection` | SaveJellyfinConnection | Connect, reconnect, or edit the Jellyfin connection | +| POST | `/api/media-sources/jellyfin/disconnect` | DisconnectJellyfin | Disconnect Jellyfin | +| GET | `/api/media-sources/jellyfin/{id}/libraries` | GetJellyfinLibraries | Get a Jellyfin source's libraries | +| PUT | `/api/media-sources/jellyfin/{id}/libraries` | ReplaceJellyfinLibraryPreferences | Replace a Jellyfin source's library sync preferences | +| GET | `/api/media-sources/jellyfin/{id}/path-replacements` | GetJellyfinPathReplacements | Get a Jellyfin source's path replacements | +| PUT | `/api/media-sources/jellyfin/{id}/path-replacements` | ReplaceJellyfinPathReplacements | Replace a Jellyfin source's path replacements | +| POST | `/api/media-sources/jellyfin/{id}/refresh-libraries` | RefreshJellyfinLibraries | Refresh a Jellyfin source's libraries | + ## Languages | Method | Path | Operation | Summary | @@ -159,6 +187,13 @@ | Method | Path | Operation | Summary | |---|---|---|---| +| GET | `/api/libraries/local` | GetLocalLibraries | Get all local libraries | +| POST | `/api/libraries/local` | | Create a local library | +| POST | `/api/libraries/local/path-exists` | | Check whether a filesystem path exists | +| POST | `/api/libraries/local/paths/{pathId}/move` | | Move a local library path to another local library | +| DELETE | `/api/libraries/local/{id}` | | Delete a local library | +| GET | `/api/libraries/local/{id}` | GetLocalLibrary | Get a local library by id | +| PUT | `/api/libraries/local/{id}` | | Update a local library | | GET | `/api/libraries/scan-status` | GetLibraryScanStatus | Get active library scan status | | POST | `/api/libraries/{id}/scan` | | Scan library | | POST | `/api/libraries/{id}/scan-show` | | Scan show | @@ -248,6 +283,19 @@ | GET | `/api/playouts/{id}/templates` | GetPlayoutTemplates | Get a block playout's templates | | PUT | `/api/playouts/{id}/templates` | | Replace a block playout's templates | +## Plex + +| Method | Path | Operation | Summary | +|---|---|---|---| +| GET | `/api/media-sources/plex` | GetPlexState | Get Plex connection state | +| POST | `/api/media-sources/plex/pin-flow` | StartPlexPinFlow | Start the Plex sign-in pin flow | +| POST | `/api/media-sources/plex/sign-out` | SignOutOfPlex | Sign out of Plex | +| GET | `/api/media-sources/plex/{id}/libraries` | GetPlexLibraries | Get a Plex server's libraries | +| PUT | `/api/media-sources/plex/{id}/libraries` | ReplacePlexLibraryPreferences | Replace a Plex server's library sync preferences | +| GET | `/api/media-sources/plex/{id}/path-replacements` | GetPlexPathReplacements | Get a Plex server's path replacements | +| PUT | `/api/media-sources/plex/{id}/path-replacements` | ReplacePlexPathReplacements | Replace a Plex server's path replacements | +| POST | `/api/media-sources/plex/{id}/refresh-libraries` | RefreshPlexLibraries | Refresh a Plex server's libraries | + ## Rerun Collections | Method | Path | Operation | Summary | diff --git a/web/src/api/generated/v1.d.ts b/web/src/api/generated/v1.d.ts index 4dc009f7e..b03c5e51d 100644 --- a/web/src/api/generated/v1.d.ts +++ b/web/src/api/generated/v1.d.ts @@ -444,6 +444,11 @@ export interface components { "playlistId": null | number; "expression": null | string; "useChaptersAsMediaItems": boolean; + }; + "CreateLocalLibraryRequest": { + "name": null | string; + "mediaKind": components["schemas"]["LibraryMediaKind"]; + "paths": null | Array; }; "CreateMultiCollectionRequest": { "name": null | string; @@ -750,6 +755,31 @@ export interface components { "LibraryScanStatusResponseModel": { "libraryId": number; "percent": number; + }; + "LocalLibraryDetailResponseModel": { + "id": number; + "name": null | string; + "mediaKind": components["schemas"]["LibraryMediaKind"]; + "isLocked": boolean; + "mediaItemCount": number; + "paths": null | Array; + }; + "LocalLibraryPathResponseModel": { + "id": number; + "path": null | string; + "mediaItemCount": number; + }; + "LocalLibraryResponseModel": { + "id": number; + "name": null | string; + "mediaKind": components["schemas"]["LibraryMediaKind"]; + "isLocked": boolean; + }; + "LocalPathCheckRequest": { + "path": null | string; + }; + "LocalPathCheckResponseModel": { + "exists": boolean; }; "LogEntryResponseModel": { "timestamp": string; @@ -841,6 +871,9 @@ export interface components { "libraries": Array; }; "MediaStreamKind": "Video" | "Audio" | "Subtitle" | "Attachment" | "ExternalSubtitle"; + "MoveLocalLibraryPathRequest": { + "targetLibraryId": number; + }; "MovieDetailResponseModel": { "id": number; "title": string; @@ -916,6 +949,16 @@ export interface components { "PagedTraktListsResponseModel": { "totalCount": number; "page": Array; + }; + "PathReplacementItemRequest": { + "id": number; + "remotePath": null | string; + "localPath": null | string; + }; + "PathReplacementResponseModel": { + "id": number; + "remotePath": null | string; + "localPath": null | string; }; "PlaybackOrder": "None" | "Chronological" | "Random" | "Shuffle" | "ShuffleInOrder" | "MultiEpisodeShuffle" | "SeasonEpisode" | "RandomRotation" | "Marathon"; "PlaylistGroupResponseModel": { @@ -1091,6 +1134,9 @@ export interface components { "endMonth": number; "endDay": number; "endYear": null | number; + }; + "PlexPinFlowResponseModel": { + "authUrl": null | string; }; "ProblemDetails": { "type"?: null | string; @@ -1107,6 +1153,30 @@ export interface components { "shuffleScheduleItems": boolean; "randomStartPoint": boolean; "fixedStartTimeBehavior": components["schemas"]["FixedStartTimeBehavior"]; + }; + "RemoteConnectionResponseModel": { + "address": null | string; + "hasApiKey": boolean; + }; + "RemoteLibraryPreferenceRequest": { + "id": number; + "shouldSyncItems": boolean; + }; + "RemoteLibraryResponseModel": { + "id": number; + "name": null | string; + "mediaKind": components["schemas"]["LibraryMediaKind"]; + "shouldSyncItems": boolean; + }; + "RemoteMediaSourceItemResponseModel": { + "id": number; + "name": null | string; + "address": null | string; + }; + "RemoteMediaSourceStateResponseModel": { + "isAuthorized": boolean; + "isLocked": boolean; + "servers": null | Array; }; "ReplaceBlockRequest": { "name": null | string; @@ -1141,6 +1211,9 @@ export interface components { "ReplaceDecoTemplateRequest": { "name": null | string; "items": null | Array; + }; + "ReplacePathReplacementsRequest": { + "items": null | Array; }; "ReplacePlaylistRequest": { "name": null | string; @@ -1151,6 +1224,9 @@ export interface components { }; "ReplacePlayoutTemplatesRequest": { "items": null | Array; + }; + "ReplaceRemoteLibraryPreferencesRequest": { + "libraries": null | Array; }; "ReplaceScheduleItemsRequest": { "items": null | Array; @@ -1181,6 +1257,10 @@ export interface components { "width": number; "height": number; "isCustom": boolean; + }; + "SaveRemoteConnectionRequest": { + "address": null | string; + "apiKey": null | string; }; "ScalingBehavior": "ScaleAndPad" | "Stretch" | "Crop"; "ScannerSettingsResponseModel": { @@ -1565,6 +1645,14 @@ export interface components { }; "UpdateImageFolderDurationResponseModel": { "durationSeconds": null | number; + }; + "UpdateLocalLibraryPathRequest": { + "id": number; + "path": null | string; + }; + "UpdateLocalLibraryRequest": { + "name": null | string; + "paths": null | Array; }; "UpdateLoggingSettingsRequest": { "defaultMinimumLogLevel": components["schemas"]["LogEventLevel"];