From 223797a6cb926a8251c493e2b319d23a246ae3bc Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 10 Jul 2026 23:54:12 +0200 Subject: [PATCH] docs+openapi: regenerate v1.json/endpoint-index/types + record flat schedule DTO decision (#126 #207 #212) - Regenerated ErsatzTV/wwwroot/openapi/v1.json, docs/endpoint-index.md, and web/src/api/generated/v1.d.ts for the flat schedule-item DTO + discovery endpoints - docs/decisions.md: flat ScheduleItemResponseModel + NamedIdResponseModel rationale - docs/api-conventions.md: shared {id,name} embed, polymorphic-VM flattening, and optional enum query-filter conventions Co-Authored-By: Claude Fable 5 --- ErsatzTV/wwwroot/openapi/v1.json | 1172 +++++++++++++++--------------- docs/api-conventions.md | 19 +- docs/decisions.md | 20 + docs/endpoint-index.md | 10 +- web/src/api/generated/v1.d.ts | 186 ++--- 5 files changed, 699 insertions(+), 708 deletions(-) diff --git a/ErsatzTV/wwwroot/openapi/v1.json b/ErsatzTV/wwwroot/openapi/v1.json index 847e90c5a..38268eea6 100644 --- a/ErsatzTV/wwwroot/openapi/v1.json +++ b/ErsatzTV/wwwroot/openapi/v1.json @@ -1158,6 +1158,86 @@ } } }, + "/api/channels/music-video-credits-templates": { + "get": { + "tags": [ + "Channels" + ], + "summary": "Get available music video credits template names", + "operationId": "GetMusicVideoCreditsTemplates", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/channels/stream-selectors": { + "get": { + "tags": [ + "Channels" + ], + "summary": "Get available channel stream selector names", + "operationId": "GetChannelStreamSelectors", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + }, "/api/channels/{id}": { "get": { "tags": [ @@ -4540,7 +4620,17 @@ "Filler Presets" ], "summary": "Get all filler presets", + "description": "Optionally filter to a single filler kind via the fillerKind query parameter.", "operationId": "GetFillerPresets", + "parameters": [ + { + "name": "fillerKind", + "in": "query", + "schema": { + "$ref": "#/components/schemas/FillerKind" + } + } + ], "responses": { "200": { "description": "OK", @@ -5208,6 +5298,46 @@ } } }, + "/api/languages": { + "get": { + "tags": [ + "Languages" + ], + "summary": "Get all available language codes", + "operationId": "GetLanguages", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LanguageCodeResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LanguageCodeResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LanguageCodeResponseModel" + } + } + } + } + } + } + } + }, "/api/libraries/scan-status": { "get": { "tags": [ @@ -9528,17 +9658,17 @@ "content": { "text/plain": { "schema": { - "$ref": "#/components/schemas/ProgramScheduleItemsWithDurationViewModel" + "$ref": "#/components/schemas/ScheduleItemsResponseModel" } }, "application/json": { "schema": { - "$ref": "#/components/schemas/ProgramScheduleItemsWithDurationViewModel" + "$ref": "#/components/schemas/ScheduleItemsResponseModel" } }, "text/json": { "schema": { - "$ref": "#/components/schemas/ProgramScheduleItemsWithDurationViewModel" + "$ref": "#/components/schemas/ScheduleItemsResponseModel" } } } @@ -9612,17 +9742,17 @@ "content": { "text/plain": { "schema": { - "$ref": "#/components/schemas/ProgramScheduleItemViewModel" + "$ref": "#/components/schemas/ScheduleItemResponseModel" } }, "application/json": { "schema": { - "$ref": "#/components/schemas/ProgramScheduleItemViewModel" + "$ref": "#/components/schemas/ScheduleItemResponseModel" } }, "text/json": { "schema": { - "$ref": "#/components/schemas/ProgramScheduleItemViewModel" + "$ref": "#/components/schemas/ScheduleItemResponseModel" } } } @@ -9718,7 +9848,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ProgramScheduleItemViewModel" + "$ref": "#/components/schemas/ScheduleItemResponseModel" } } }, @@ -9726,7 +9856,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ProgramScheduleItemViewModel" + "$ref": "#/components/schemas/ScheduleItemResponseModel" } } }, @@ -9734,7 +9864,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ProgramScheduleItemViewModel" + "$ref": "#/components/schemas/ScheduleItemResponseModel" } } } @@ -17569,42 +17699,10 @@ } }, "FillerPresetResponseModel": { - "required": [ - "id", - "name" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "name": { - "type": [ - "null", - "string" - ] - } - } - }, - "FillerPresetViewModel": { "required": [ "id", "name", - "fillerKind", - "fillerMode", - "duration", - "count", - "padToNearestMinute", - "allowWatermarks", - "collectionType", - "collectionId", - "mediaItemId", - "multiCollectionId", - "smartCollectionId", - "playlist", - "expression", - "useChaptersAsMediaItems" + "fillerKind" ], "type": "object", "properties": { @@ -17620,76 +17718,6 @@ }, "fillerKind": { "$ref": "#/components/schemas/FillerKind" - }, - "fillerMode": { - "$ref": "#/components/schemas/FillerMode" - }, - "duration": { - "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$", - "type": [ - "null", - "string" - ] - }, - "count": { - "type": [ - "null", - "integer" - ], - "format": "int32" - }, - "padToNearestMinute": { - "type": [ - "null", - "integer" - ], - "format": "int32" - }, - "allowWatermarks": { - "type": "boolean" - }, - "collectionType": { - "$ref": "#/components/schemas/CollectionType" - }, - "collectionId": { - "type": [ - "null", - "integer" - ], - "format": "int32" - }, - "mediaItemId": { - "type": [ - "null", - "integer" - ], - "format": "int32" - }, - "multiCollectionId": { - "type": [ - "null", - "integer" - ], - "format": "int32" - }, - "smartCollectionId": { - "type": [ - "null", - "integer" - ], - "format": "int32" - }, - "playlist": { - "$ref": "#/components/schemas/PlaylistViewModel" - }, - "expression": { - "type": [ - "null", - "string" - ] - }, - "useChaptersAsMediaItems": { - "type": "boolean" } } }, @@ -17734,32 +17762,6 @@ } } }, - "GraphicsElementViewModel": { - "required": [ - "id", - "name", - "fileName" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "name": { - "type": [ - "null", - "string" - ] - }, - "fileName": { - "type": [ - "null", - "string" - ] - } - } - }, "GuideMode": { "enum": [ "Normal", @@ -17896,6 +17898,27 @@ } } }, + "LanguageCodeResponseModel": { + "required": [ + "code", + "englishName" + ], + "type": "object", + "properties": { + "code": { + "type": [ + "null", + "string" + ] + }, + "englishName": { + "type": [ + "null", + "string" + ] + } + } + }, "LibraryBrowseItemResponseModel": { "required": [ "id", @@ -18738,30 +18761,6 @@ } } }, - "MultiCollectionItemViewModel": { - "required": [ - "multiCollectionId", - "collection", - "scheduleAsGroup", - "playbackOrder" - ], - "type": "object", - "properties": { - "multiCollectionId": { - "type": "integer", - "format": "int32" - }, - "collection": { - "$ref": "#/components/schemas/MediaCollectionViewModel" - }, - "scheduleAsGroup": { - "type": "boolean" - }, - "playbackOrder": { - "$ref": "#/components/schemas/PlaybackOrder" - } - } - }, "MultiCollectionResponseModel": { "required": [ "id", @@ -18785,69 +18784,6 @@ } } }, - "MultiCollectionSmartItemViewModel": { - "required": [ - "multiCollectionId", - "smartCollection", - "scheduleAsGroup", - "playbackOrder" - ], - "type": "object", - "properties": { - "multiCollectionId": { - "type": "integer", - "format": "int32" - }, - "smartCollection": { - "$ref": "#/components/schemas/SmartCollectionViewModel" - }, - "scheduleAsGroup": { - "type": "boolean" - }, - "playbackOrder": { - "$ref": "#/components/schemas/PlaybackOrder" - } - } - }, - "MultiCollectionViewModel": { - "required": [ - "id", - "name", - "items", - "smartItems" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "name": { - "type": [ - "null", - "string" - ] - }, - "items": { - "type": [ - "null", - "array" - ], - "items": { - "$ref": "#/components/schemas/MultiCollectionItemViewModel" - } - }, - "smartItems": { - "type": [ - "null", - "array" - ], - "items": { - "$ref": "#/components/schemas/MultiCollectionSmartItemViewModel" - } - } - } - }, "MultipleMode": { "enum": [ "Count", @@ -18857,14 +18793,14 @@ ], "type": "string" }, - "NamedMediaItemViewModel": { + "NamedIdResponseModel": { "required": [ - "mediaItemId", + "id", "name" ], "type": "object", "properties": { - "mediaItemId": { + "id": { "type": "integer", "format": "int32" }, @@ -19309,34 +19245,6 @@ } } }, - "PlaylistViewModel": { - "required": [ - "id", - "playlistGroupId", - "name", - "isSystem" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "playlistGroupId": { - "type": "integer", - "format": "int32" - }, - "name": { - "type": [ - "null", - "string" - ] - }, - "isSystem": { - "type": "boolean" - } - } - }, "PlayoutAlternateScheduleItemRequest": { "required": [ "id", @@ -20071,205 +19979,6 @@ } } }, - "ProgramScheduleItemsWithDurationViewModel": { - "required": [ - "items", - "totalDurationEstimate" - ], - "type": "object", - "properties": { - "items": { - "type": [ - "null", - "array" - ], - "items": { - "$ref": "#/components/schemas/ProgramScheduleItemViewModel" - } - }, - "totalDurationEstimate": { - "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$", - "type": [ - "null", - "string" - ] - } - } - }, - "ProgramScheduleItemViewModel": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "index": { - "type": "integer", - "format": "int32" - }, - "startType": { - "$ref": "#/components/schemas/StartType" - }, - "startTime": { - "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$", - "type": [ - "null", - "string" - ] - }, - "fixedStartTimeBehavior": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/FixedStartTimeBehavior" - } - ] - }, - "playoutMode": { - "$ref": "#/components/schemas/PlayoutMode" - }, - "collectionType": { - "$ref": "#/components/schemas/CollectionType" - }, - "collection": { - "$ref": "#/components/schemas/MediaCollectionViewModel" - }, - "multiCollection": { - "$ref": "#/components/schemas/MultiCollectionViewModel" - }, - "smartCollection": { - "$ref": "#/components/schemas/SmartCollectionViewModel" - }, - "rerunCollection": { - "$ref": "#/components/schemas/RerunCollectionViewModel" - }, - "playlist": { - "$ref": "#/components/schemas/PlaylistViewModel" - }, - "mediaItem": { - "$ref": "#/components/schemas/NamedMediaItemViewModel" - }, - "searchTitle": { - "type": [ - "null", - "string" - ] - }, - "searchQuery": { - "type": [ - "null", - "string" - ] - }, - "playbackOrder": { - "$ref": "#/components/schemas/PlaybackOrder" - }, - "marathonGroupBy": { - "$ref": "#/components/schemas/MarathonGroupBy" - }, - "marathonShuffleGroups": { - "type": "boolean" - }, - "marathonShuffleItems": { - "type": "boolean" - }, - "marathonBatchSize": { - "type": [ - "null", - "integer" - ], - "format": "int32" - }, - "fillWithGroupMode": { - "$ref": "#/components/schemas/FillWithGroupMode" - }, - "customTitle": { - "type": [ - "null", - "string" - ] - }, - "guideMode": { - "$ref": "#/components/schemas/GuideMode" - }, - "preRollFiller": { - "$ref": "#/components/schemas/FillerPresetViewModel" - }, - "midRollFiller": { - "$ref": "#/components/schemas/FillerPresetViewModel" - }, - "postRollFiller": { - "$ref": "#/components/schemas/FillerPresetViewModel" - }, - "tailFiller": { - "$ref": "#/components/schemas/FillerPresetViewModel" - }, - "fallbackFiller": { - "$ref": "#/components/schemas/FillerPresetViewModel" - }, - "watermarks": { - "type": [ - "null", - "array" - ], - "items": { - "$ref": "#/components/schemas/WatermarkViewModel" - } - }, - "graphicsElements": { - "type": [ - "null", - "array" - ], - "items": { - "$ref": "#/components/schemas/GraphicsElementViewModel" - } - }, - "preferredAudioLanguageCode": { - "type": [ - "null", - "string" - ] - }, - "preferredAudioTitle": { - "type": [ - "null", - "string" - ] - }, - "preferredSubtitleLanguageCode": { - "type": [ - "null", - "string" - ] - }, - "subtitleMode": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/ChannelSubtitleMode" - } - ] - }, - "durationEstimate": { - "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$", - "type": [ - "null", - "string" - ] - }, - "name": { - "type": [ - "null", - "string" - ] - } - } - }, "ProgramScheduleViewModel": { "required": [ "id", @@ -20658,53 +20367,6 @@ } } }, - "RerunCollectionViewModel": { - "required": [ - "id", - "name", - "collectionType", - "collection", - "multiCollection", - "smartCollection", - "mediaItem", - "firstRunPlaybackOrder", - "rerunPlaybackOrder" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "name": { - "type": [ - "null", - "string" - ] - }, - "collectionType": { - "$ref": "#/components/schemas/CollectionType" - }, - "collection": { - "$ref": "#/components/schemas/MediaCollectionViewModel" - }, - "multiCollection": { - "$ref": "#/components/schemas/MultiCollectionViewModel" - }, - "smartCollection": { - "$ref": "#/components/schemas/SmartCollectionViewModel" - }, - "mediaItem": { - "$ref": "#/components/schemas/NamedMediaItemViewModel" - }, - "firstRunPlaybackOrder": { - "$ref": "#/components/schemas/PlaybackOrder" - }, - "rerunPlaybackOrder": { - "$ref": "#/components/schemas/PlaybackOrder" - } - } - }, "ResolutionResponseModel": { "required": [ "id", @@ -21070,6 +20732,424 @@ } } }, + "ScheduleItemResponseModel": { + "required": [ + "id", + "index", + "startType", + "startTime", + "fixedStartTimeBehavior", + "playoutMode", + "collectionType", + "collectionId", + "multiCollectionId", + "smartCollectionId", + "rerunCollectionId", + "mediaItemId", + "playlistId", + "searchTitle", + "searchQuery", + "playbackOrder", + "marathonGroupBy", + "marathonShuffleGroups", + "marathonShuffleItems", + "marathonBatchSize", + "fillWithGroupMode", + "multipleMode", + "multipleCount", + "playoutDuration", + "tailMode", + "discardToFillAttempts", + "customTitle", + "guideMode", + "preRollFillerId", + "midRollFillerId", + "postRollFillerId", + "tailFillerId", + "fallbackFillerId", + "watermarkIds", + "graphicsElementIds", + "preferredAudioLanguageCode", + "preferredAudioTitle", + "preferredSubtitleLanguageCode", + "subtitleMode", + "collectionName", + "multiCollectionName", + "smartCollectionName", + "rerunCollectionName", + "playlistName", + "playlistGroupId", + "mediaItemName", + "preRollFillerName", + "midRollFillerName", + "postRollFillerName", + "tailFillerName", + "fallbackFillerName", + "watermarks", + "graphicsElements", + "name", + "durationEstimate" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "index": { + "type": "integer", + "format": "int32" + }, + "startType": { + "$ref": "#/components/schemas/StartType" + }, + "startTime": { + "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$", + "type": [ + "null", + "string" + ] + }, + "fixedStartTimeBehavior": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/FixedStartTimeBehavior" + } + ] + }, + "playoutMode": { + "$ref": "#/components/schemas/PlayoutMode" + }, + "collectionType": { + "$ref": "#/components/schemas/CollectionType" + }, + "collectionId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "multiCollectionId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "smartCollectionId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "rerunCollectionId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "mediaItemId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "playlistId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "searchTitle": { + "type": [ + "null", + "string" + ] + }, + "searchQuery": { + "type": [ + "null", + "string" + ] + }, + "playbackOrder": { + "$ref": "#/components/schemas/PlaybackOrder" + }, + "marathonGroupBy": { + "$ref": "#/components/schemas/MarathonGroupBy" + }, + "marathonShuffleGroups": { + "type": "boolean" + }, + "marathonShuffleItems": { + "type": "boolean" + }, + "marathonBatchSize": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "fillWithGroupMode": { + "$ref": "#/components/schemas/FillWithGroupMode" + }, + "multipleMode": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MultipleMode" + } + ] + }, + "multipleCount": { + "type": [ + "null", + "string" + ] + }, + "playoutDuration": { + "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$", + "type": [ + "null", + "string" + ] + }, + "tailMode": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/TailMode" + } + ] + }, + "discardToFillAttempts": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "customTitle": { + "type": [ + "null", + "string" + ] + }, + "guideMode": { + "$ref": "#/components/schemas/GuideMode" + }, + "preRollFillerId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "midRollFillerId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "postRollFillerId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "tailFillerId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "fallbackFillerId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "watermarkIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "graphicsElementIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "preferredAudioLanguageCode": { + "type": [ + "null", + "string" + ] + }, + "preferredAudioTitle": { + "type": [ + "null", + "string" + ] + }, + "preferredSubtitleLanguageCode": { + "type": [ + "null", + "string" + ] + }, + "subtitleMode": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ChannelSubtitleMode" + } + ] + }, + "collectionName": { + "type": [ + "null", + "string" + ] + }, + "multiCollectionName": { + "type": [ + "null", + "string" + ] + }, + "smartCollectionName": { + "type": [ + "null", + "string" + ] + }, + "rerunCollectionName": { + "type": [ + "null", + "string" + ] + }, + "playlistName": { + "type": [ + "null", + "string" + ] + }, + "playlistGroupId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "mediaItemName": { + "type": [ + "null", + "string" + ] + }, + "preRollFillerName": { + "type": [ + "null", + "string" + ] + }, + "midRollFillerName": { + "type": [ + "null", + "string" + ] + }, + "postRollFillerName": { + "type": [ + "null", + "string" + ] + }, + "tailFillerName": { + "type": [ + "null", + "string" + ] + }, + "fallbackFillerName": { + "type": [ + "null", + "string" + ] + }, + "watermarks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NamedIdResponseModel" + } + }, + "graphicsElements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NamedIdResponseModel" + } + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "durationEstimate": { + "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$", + "type": [ + "null", + "string" + ] + } + } + }, + "ScheduleItemsResponseModel": { + "required": [ + "items", + "totalDurationEstimate" + ], + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleItemResponseModel" + } + }, + "totalDurationEstimate": { + "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$", + "type": [ + "null", + "string" + ] + } + } + }, "SchedulingPickerOptionResponseModel": { "required": [ "id", @@ -23071,91 +23151,6 @@ ], "type": "string" }, - "WatermarkViewModel": { - "required": [ - "id", - "image", - "name", - "mode", - "imageSource", - "location", - "size", - "width", - "horizontalMargin", - "verticalMargin", - "frequencyMinutes", - "durationSeconds", - "opacity", - "placeWithinSourceContent", - "opacityExpression", - "zIndex" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "image": { - "$ref": "#/components/schemas/ArtworkContentTypeModel" - }, - "name": { - "type": [ - "null", - "string" - ] - }, - "mode": { - "$ref": "#/components/schemas/ChannelWatermarkMode" - }, - "imageSource": { - "$ref": "#/components/schemas/ChannelWatermarkImageSource" - }, - "location": { - "$ref": "#/components/schemas/WatermarkLocation" - }, - "size": { - "$ref": "#/components/schemas/WatermarkSize" - }, - "width": { - "type": "number", - "format": "double" - }, - "horizontalMargin": { - "type": "number", - "format": "double" - }, - "verticalMargin": { - "type": "number", - "format": "double" - }, - "frequencyMinutes": { - "type": "integer", - "format": "int32" - }, - "durationSeconds": { - "type": "integer", - "format": "int32" - }, - "opacity": { - "type": "integer", - "format": "int32" - }, - "placeWithinSourceContent": { - "type": "boolean" - }, - "opacityExpression": { - "type": [ - "null", - "string" - ] - }, - "zIndex": { - "type": "integer", - "format": "int32" - } - } - }, "XmltvBlockBehavior": { "enum": [ "SplitTimeEvenly", @@ -23235,6 +23230,9 @@ { "name": "Images" }, + { + "name": "Languages" + }, { "name": "Libraries" }, diff --git a/docs/api-conventions.md b/docs/api-conventions.md index 2a89fe5b0..e3b89e566 100644 --- a/docs/api-conventions.md +++ b/docs/api-conventions.md @@ -52,7 +52,24 @@ Exemplars: `ToReplaceCommand(int index)`) method that maps it to the Application-layer command type. - **`ErsatzTV.Application` has no nullable context** (no `` = C# default `disable` for that TFM in this repo — confirms CS8632 would otherwise fire) — do **not** add `?` nullable - annotations to types living there; that's a Core/Api-layer-only convention. + annotations to types living there; that's a Core/Api-layer-only convention. A static mapper that + lives in `ErsatzTV.Application` but returns a Core/Api response DTO with nullable members is fine + (e.g. `ScheduleItemResponseMapper`) — the nullability lives on the DTO record, not the mapper. +- **Shared `{id, name}` embeds**: use `ErsatzTV.Core/Api/NamedIdResponseModel.cs` + (`record NamedIdResponseModel(int Id, string Name)`) when a response DTO needs to embed a list of + named references (e.g. a schedule item's `watermarks` / `graphicsElements`) rather than minting a + one-off `(int, string)` record per domain. +- **Flatten polymorphic VMs for the wire**: when an Application ViewModel is an abstract/polymorphic + record (subtypes carrying extra fields), the OpenAPI schema only captures the base shape — promote + every subtype field to a nullable top-level member on a flat response DTO and pattern-match the + concrete VM in the mapper. Exemplar: `ScheduleItemResponseModel` (issue #126, see + `docs/decisions.md` 2026-07-10). Keep the flat DTO's **mutation** fields named 1:1 with the + matching request DTO so GET→PUT is lossless (guard with a round-trip handler test). +- **Optional enum filter via query param**: to filter a list endpoint by an enum, add a nullable + enum parameter to the query record (default `null`) and bind it with `[FromQuery] TEnum? name` on + the action; filter server-side only when it has a value. Exemplar: `?fillerKind=` on + `GET /api/filler-presets` (`GetAllFillerPresetsForApi(FillerKind? FillerKind = null)`). An invalid + enum value is rejected by model binding (400) — no handler-side guard needed. ## 3. Error mapping diff --git a/docs/decisions.md b/docs/decisions.md index 7dc4f6dec..21253eecd 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -225,3 +225,23 @@ TelevisionShow/TelevisionSeason/Artist per-media-item CollectionTypes and 422s t "Add All" (query-wide) mirrors Blazor's two-step: materialize ids via `GET /api/search/all-items`, then reuse the id-list add endpoints — no query-based add command exists server-side. Issues #208/#209. + +## 2026-07-10 — Schedule-item GET returns a flat, non-polymorphic DTO (`ScheduleItemResponseModel`) + +`GET/POST/PUT /api/schedules/{id}/items` return `ScheduleItemResponseModel` / +`ScheduleItemsResponseModel` (`ErsatzTV.Core/Api/Scheduling/`), **not** the Application-layer +`ProgramScheduleItemViewModel` hierarchy (One/Flood/Multiple/Duration subtypes). The polymorphic VM +only described its base shape in OpenAPI, so the SPA couldn't see the subtype fields (issue #126). +The flat DTO promotes every subtype field to a nullable top-level member — `multipleMode`, +`multipleCount` (renamed from the VM's `Count`), `playoutDuration`, `tailMode`, +`discardToFillAttempts` — mapped by pattern-matching the concrete VM in +`ScheduleItemResponseMapper` (`ErsatzTV.Application/ProgramSchedules/`). Its **mutation fields are +named 1:1 with `ScheduleItemRequest`** so a GET maps losslessly back to a PUT/POST +(`ScheduleItemResponseRoundTripTests` is the release gate proving the fixed point). It also carries +picker-hydration fields the editor needs: `collectionName`/`smartCollectionName`/…/`playlistName`, +`playlistGroupId` (to preselect the playlist's group), per-filler names, `watermarks` / +`graphicsElements` as `NamedIdResponseModel` lists, the computed `name`, and `durationEstimate`. +`GetProgramScheduleItemsHandler.EnforceProperties` still rewrites StartType→Dynamic, Flood→One and +Playlist/Rerun→PlaybackOrder None when `ShuffleScheduleItems` is on — that lossy normalization is +deliberate and lives on the read side (documented + tested). New shared `NamedIdResponseModel` +(`ErsatzTV.Core/Api/`) is the generic `{id, name}` embed for API responses. Issues #126/#207/#212. diff --git a/docs/endpoint-index.md b/docs/endpoint-index.md index 1428189cb..0b50cc62c 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`.* -130 endpoints, 204 operations. +133 endpoints, 207 operations. ## Artists @@ -59,7 +59,9 @@ | POST | `/api/channels/bulk/group` | | Move channels to a group | | POST | `/api/channels/bulk/renumber` | | Renumber channels | | POST | `/api/channels/from-lineup` | CreateChannelFromLineup | Create a channel from a library lineup | +| GET | `/api/channels/music-video-credits-templates` | GetMusicVideoCreditsTemplates | Get available music video credits template names | | GET | `/api/channels/state` | | Get channel runtime state | +| GET | `/api/channels/stream-selectors` | GetChannelStreamSelectors | Get available channel stream selector names | | POST | `/api/channels/{channelNumber}/playout/reset` | | Reset channel playout | | DELETE | `/api/channels/{id}` | | Delete a channel | | GET | `/api/channels/{id}` | GetChannelById | Get a channel by id | @@ -147,6 +149,12 @@ | 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 | +## Languages + +| Method | Path | Operation | Summary | +|---|---|---|---| +| GET | `/api/languages` | GetLanguages | Get all available language codes | + ## Libraries | Method | Path | Operation | Summary | diff --git a/web/src/api/generated/v1.d.ts b/web/src/api/generated/v1.d.ts index 9c9024447..cf8339c6a 100644 --- a/web/src/api/generated/v1.d.ts +++ b/web/src/api/generated/v1.d.ts @@ -685,24 +685,7 @@ export interface components { "FillerPresetResponseModel": { "id": number; "name": null | string; - }; - "FillerPresetViewModel": { - "id": number; - "name": null | string; "fillerKind": components["schemas"]["FillerKind"]; - "fillerMode": components["schemas"]["FillerMode"]; - "duration": null | string; - "count": null | number; - "padToNearestMinute": null | number; - "allowWatermarks": boolean; - "collectionType": components["schemas"]["CollectionType"]; - "collectionId": null | number; - "mediaItemId": null | number; - "multiCollectionId": null | number; - "smartCollectionId": null | number; - "playlist": components["schemas"]["PlaylistViewModel"]; - "expression": null | string; - "useChaptersAsMediaItems": boolean; }; "FillWithGroupMode": "None" | "FillWithOrderedGroups" | "FillWithShuffledGroups"; "FilterMode": "HardwareIfPossible" | "Software"; @@ -710,11 +693,6 @@ export interface components { "GraphicsElementResponseModel": { "id": number; "name": null | string; - }; - "GraphicsElementViewModel": { - "id": number; - "name": null | string; - "fileName": null | string; }; "GuideMode": "Normal" | "Filler"; "HardwareAccelerationKind": "None" | "Qsv" | "Nvenc" | "Vaapi" | "VideoToolbox" | "Amf" | "V4l2m2m" | "Rkmpp"; @@ -742,6 +720,10 @@ export interface components { "subfolderCount": number; "imageCount": number; "durationSeconds": null | number; + }; + "LanguageCodeResponseModel": { + "code": null | string; + "englishName": null | string; }; "LibraryBrowseItemResponseModel": { "id": number; @@ -889,33 +871,15 @@ export interface components { "name": string; "scheduleAsGroup": boolean; "playbackOrder": components["schemas"]["PlaybackOrder"]; - }; - "MultiCollectionItemViewModel": { - "multiCollectionId": number; - "collection": components["schemas"]["MediaCollectionViewModel"]; - "scheduleAsGroup": boolean; - "playbackOrder": components["schemas"]["PlaybackOrder"]; }; "MultiCollectionResponseModel": { "id": number; "name": string; "items": Array; - }; - "MultiCollectionSmartItemViewModel": { - "multiCollectionId": number; - "smartCollection": components["schemas"]["SmartCollectionViewModel"]; - "scheduleAsGroup": boolean; - "playbackOrder": components["schemas"]["PlaybackOrder"]; - }; - "MultiCollectionViewModel": { - "id": number; - "name": null | string; - "items": null | Array; - "smartItems": null | Array; }; "MultipleMode": "Count" | "CollectionSize" | "PlaylistItemSize" | "MultiEpisodeGroupSize"; - "NamedMediaItemViewModel": { - "mediaItemId": number; + "NamedIdResponseModel": { + "id": number; "name": null | string; }; "NormalizeLoudnessMode": "Off" | "LoudNorm"; @@ -999,12 +963,6 @@ export interface components { "playlistGroupId": number; "name": string; "isSystem": boolean; - }; - "PlaylistViewModel": { - "id": number; - "playlistGroupId": number; - "name": null | string; - "isSystem": boolean; }; "PlayoutAlternateScheduleItemRequest": { "id": number; @@ -1138,48 +1096,6 @@ export interface components { "status"?: null | number; "detail"?: null | string; "instance"?: null | string; - }; - "ProgramScheduleItemsWithDurationViewModel": { - "items": null | Array; - "totalDurationEstimate": null | string; - }; - "ProgramScheduleItemViewModel": { - "id"?: number; - "index"?: number; - "startType"?: components["schemas"]["StartType"]; - "startTime"?: null | string; - "fixedStartTimeBehavior"?: null | components["schemas"]["FixedStartTimeBehavior"]; - "playoutMode"?: components["schemas"]["PlayoutMode"]; - "collectionType"?: components["schemas"]["CollectionType"]; - "collection"?: components["schemas"]["MediaCollectionViewModel"]; - "multiCollection"?: components["schemas"]["MultiCollectionViewModel"]; - "smartCollection"?: components["schemas"]["SmartCollectionViewModel"]; - "rerunCollection"?: components["schemas"]["RerunCollectionViewModel"]; - "playlist"?: components["schemas"]["PlaylistViewModel"]; - "mediaItem"?: components["schemas"]["NamedMediaItemViewModel"]; - "searchTitle"?: null | string; - "searchQuery"?: null | string; - "playbackOrder"?: components["schemas"]["PlaybackOrder"]; - "marathonGroupBy"?: components["schemas"]["MarathonGroupBy"]; - "marathonShuffleGroups"?: boolean; - "marathonShuffleItems"?: boolean; - "marathonBatchSize"?: null | number; - "fillWithGroupMode"?: components["schemas"]["FillWithGroupMode"]; - "customTitle"?: null | string; - "guideMode"?: components["schemas"]["GuideMode"]; - "preRollFiller"?: components["schemas"]["FillerPresetViewModel"]; - "midRollFiller"?: components["schemas"]["FillerPresetViewModel"]; - "postRollFiller"?: components["schemas"]["FillerPresetViewModel"]; - "tailFiller"?: components["schemas"]["FillerPresetViewModel"]; - "fallbackFiller"?: components["schemas"]["FillerPresetViewModel"]; - "watermarks"?: null | Array; - "graphicsElements"?: null | Array; - "preferredAudioLanguageCode"?: null | string; - "preferredAudioTitle"?: null | string; - "preferredSubtitleLanguageCode"?: null | string; - "subtitleMode"?: null | components["schemas"]["ChannelSubtitleMode"]; - "durationEstimate"?: null | string; - "name"?: null | string; }; "ProgramScheduleViewModel": { "id": number; @@ -1249,17 +1165,6 @@ export interface components { "selectedName": null | string; "firstRunPlaybackOrder": components["schemas"]["PlaybackOrder"]; "rerunPlaybackOrder": components["schemas"]["PlaybackOrder"]; - }; - "RerunCollectionViewModel": { - "id": number; - "name": null | string; - "collectionType": components["schemas"]["CollectionType"]; - "collection": components["schemas"]["MediaCollectionViewModel"]; - "multiCollection": components["schemas"]["MultiCollectionViewModel"]; - "smartCollection": components["schemas"]["SmartCollectionViewModel"]; - "mediaItem": components["schemas"]["NamedMediaItemViewModel"]; - "firstRunPlaybackOrder": components["schemas"]["PlaybackOrder"]; - "rerunPlaybackOrder": components["schemas"]["PlaybackOrder"]; }; "ResolutionResponseModel": { "id": number; @@ -1321,6 +1226,67 @@ export interface components { "preferredAudioTitle": null | string; "preferredSubtitleLanguageCode": null | string; "subtitleMode": null | components["schemas"]["ChannelSubtitleMode"]; + }; + "ScheduleItemResponseModel": { + "id": number; + "index": number; + "startType": components["schemas"]["StartType"]; + "startTime": null | string; + "fixedStartTimeBehavior": null | components["schemas"]["FixedStartTimeBehavior"]; + "playoutMode": components["schemas"]["PlayoutMode"]; + "collectionType": components["schemas"]["CollectionType"]; + "collectionId": null | number; + "multiCollectionId": null | number; + "smartCollectionId": null | number; + "rerunCollectionId": null | number; + "mediaItemId": null | number; + "playlistId": null | number; + "searchTitle": null | string; + "searchQuery": null | string; + "playbackOrder": components["schemas"]["PlaybackOrder"]; + "marathonGroupBy": components["schemas"]["MarathonGroupBy"]; + "marathonShuffleGroups": boolean; + "marathonShuffleItems": boolean; + "marathonBatchSize": null | number; + "fillWithGroupMode": components["schemas"]["FillWithGroupMode"]; + "multipleMode": null | components["schemas"]["MultipleMode"]; + "multipleCount": null | string; + "playoutDuration": null | string; + "tailMode": null | components["schemas"]["TailMode"]; + "discardToFillAttempts": null | number; + "customTitle": null | string; + "guideMode": components["schemas"]["GuideMode"]; + "preRollFillerId": null | number; + "midRollFillerId": null | number; + "postRollFillerId": null | number; + "tailFillerId": null | number; + "fallbackFillerId": null | number; + "watermarkIds": Array; + "graphicsElementIds": Array; + "preferredAudioLanguageCode": null | string; + "preferredAudioTitle": null | string; + "preferredSubtitleLanguageCode": null | string; + "subtitleMode": null | components["schemas"]["ChannelSubtitleMode"]; + "collectionName": null | string; + "multiCollectionName": null | string; + "smartCollectionName": null | string; + "rerunCollectionName": null | string; + "playlistName": null | string; + "playlistGroupId": null | number; + "mediaItemName": null | string; + "preRollFillerName": null | string; + "midRollFillerName": null | string; + "postRollFillerName": null | string; + "tailFillerName": null | string; + "fallbackFillerName": null | string; + "watermarks": Array; + "graphicsElements": Array; + "name": null | string; + "durationEstimate": null | string; + }; + "ScheduleItemsResponseModel": { + "items": Array; + "totalDurationEstimate": null | string; }; "SchedulingPickerOptionResponseModel": { "id": number; @@ -1711,24 +1677,6 @@ export interface components { "name": null | string; }; "WatermarkSize": "Scaled" | "ActualSize"; - "WatermarkViewModel": { - "id": number; - "image": components["schemas"]["ArtworkContentTypeModel"]; - "name": null | string; - "mode": components["schemas"]["ChannelWatermarkMode"]; - "imageSource": components["schemas"]["ChannelWatermarkImageSource"]; - "location": components["schemas"]["WatermarkLocation"]; - "size": components["schemas"]["WatermarkSize"]; - "width": number; - "horizontalMargin": number; - "verticalMargin": number; - "frequencyMinutes": number; - "durationSeconds": number; - "opacity": number; - "placeWithinSourceContent": boolean; - "opacityExpression": null | string; - "zIndex": number; - }; "XmltvBlockBehavior": "SplitTimeEvenly" | "UseActualTimes"; "XmltvSettingsResponseModel": { "daysToBuild": number;