feat(api): #288 ChannelDetailResponseModel for edit form + SPA repoints + final regen

Mint ChannelDetailResponseModel (faithful detail DTO exposing the raw editable
field set the channel editor reads: raw FFmpegProfileId/WatermarkId/FallbackFillerId
ids, the mode enums, logo, playoutCount, id) and route GetById/Create/Update through
it, replacing the lean list ChannelResponseModel that resolved the profile to a name
and dropped the editable ids (a functional regression for draftFromChannel). The lean
ChannelResponseModel stays unchanged for GET /api/channels. webEncodedName dropped
(SPA never reads it). Logo is mirrored as a Core ChannelLogoResponseModel since the
Application ArtworkContentTypeModel can't be referenced from Core.

Repoint the hand-written SPA client aliases now that the VMs are gone from the schema:
Channel -> ChannelDetailResponseModel, MediaCollection/SmartCollection -> *ResponseModel,
ProgramSchedule -> ProgramScheduleResponseModel. Fix #288 honest-nullability test fallout
in search.test.ts (null -> [] for now-non-null id arrays). Include the already-on-disk
playouts.ts WithDayNames removal and regenerate v1.json + v1.d.ts + endpoint-index.md
(authoritative final regen; the reset endpoint's {channelNumber}->{id} re-key surfaces
in the generated docs and the OpenApi error-contract test).

Refs #288 #197

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 02:33:39 +02:00
co-authored by Claude Opus 4.8
parent 74170611da
commit 70f357f8f9
17 changed files with 5806 additions and 983 deletions
+87 -92
View File
@@ -134,6 +134,37 @@ export interface components {
};
"BulkRenumberChannelsRequest": {
"channels": Array<components["schemas"]["BulkRenumberChannelRequest"]>;
};
"ChannelDetailResponseModel": {
"id": number;
"number": string;
"name": string;
"group": string;
"categories": string;
"ffmpegProfileId": number;
"slugSeconds": null | number;
"logo": components["schemas"]["ChannelLogoResponseModel"];
"streamSelectorMode": components["schemas"]["ChannelStreamSelectorMode"];
"streamSelector": string;
"preferredAudioLanguageCode": string;
"preferredAudioTitle": string;
"playoutSource": components["schemas"]["ChannelPlayoutSource"];
"playoutMode": components["schemas"]["ChannelPlayoutMode"];
"mirrorSourceChannelId": null | number;
"playoutOffset": null | string;
"streamingMode": components["schemas"]["StreamingMode"];
"watermarkId": null | number;
"fallbackFillerId": null | number;
"playoutCount": number;
"preferredSubtitleLanguageCode": string;
"subtitleMode": components["schemas"]["ChannelSubtitleMode"];
"musicVideoCreditsMode": components["schemas"]["ChannelMusicVideoCreditsMode"];
"musicVideoCreditsTemplate": string;
"songVideoMode": components["schemas"]["ChannelSongVideoMode"];
"transcodeMode": components["schemas"]["ChannelTranscodeMode"];
"idleBehavior": components["schemas"]["ChannelIdleBehavior"];
"isEnabled": boolean;
"showInEpg": boolean;
};
"ChannelGuideChannelResponseModel": {
"number": string;
@@ -154,6 +185,13 @@ export interface components {
"channels": Array<components["schemas"]["ChannelGuideChannelResponseModel"]>;
};
"ChannelIdleBehavior": "StopOnDisconnect" | "KeepRunning";
"ChannelLogoResponseModel": {
"path": string;
"contentType": string;
"isExternalUrl"?: boolean;
"hasContentType"?: boolean;
"urlWithContentType"?: null | string;
};
"ChannelMusicVideoCreditsMode": "None" | "GenerateSubtitles";
"ChannelNowPlayingResponseModel": {
"title": string;
@@ -215,38 +253,6 @@ export interface components {
"fixedStartTimeBehavior": components["schemas"]["FixedStartTimeBehavior"];
};
"ChannelTranscodeMode": "OnDemand";
"ChannelViewModel": {
"id": number;
"number": null | string;
"name": null | string;
"group": null | string;
"categories": null | string;
"ffmpegProfileId": number;
"slugSeconds": null | number;
"logo": components["schemas"]["ArtworkContentTypeModel"];
"streamSelectorMode": components["schemas"]["ChannelStreamSelectorMode"];
"streamSelector": null | string;
"preferredAudioLanguageCode": null | string;
"preferredAudioTitle": null | string;
"playoutSource": components["schemas"]["ChannelPlayoutSource"];
"playoutMode": components["schemas"]["ChannelPlayoutMode"];
"mirrorSourceChannelId": null | number;
"playoutOffset": null | string;
"streamingMode": components["schemas"]["StreamingMode"];
"watermarkId": null | number;
"fallbackFillerId": null | number;
"playoutCount": number;
"preferredSubtitleLanguageCode": null | string;
"subtitleMode": components["schemas"]["ChannelSubtitleMode"];
"musicVideoCreditsMode": components["schemas"]["ChannelMusicVideoCreditsMode"];
"musicVideoCreditsTemplate": null | string;
"songVideoMode": components["schemas"]["ChannelSongVideoMode"];
"transcodeMode": components["schemas"]["ChannelTranscodeMode"];
"idleBehavior": components["schemas"]["ChannelIdleBehavior"];
"isEnabled": boolean;
"showInEpg": boolean;
"webEncodedName"?: null | string;
};
"ChannelWatermarkImageSource": "Custom" | "ChannelLogo" | "Resource";
"ChannelWatermarkMode": "None" | "Permanent" | "Intermittent" | "OpacityExpression";
"CollectionType": "Collection" | "TelevisionShow" | "TelevisionSeason" | "Artist" | "MultiCollection" | "SmartCollection" | "Playlist" | "RerunFirstRun" | "RerunRerun" | "SearchQuery" | "Movie" | "Episode" | "MusicVideo" | "OtherVideo" | "Song" | "Image" | "RemoteStream" | "FakeCollection" | "FakePlaylistItem";
@@ -515,7 +521,7 @@ export interface components {
"zIndex": number;
"placeWithinSourceContent": boolean;
};
"DayOfWeek": number;
"DayOfWeek": "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday";
"DecoBreakContentRequest": {
"id": number;
"collectionType": components["schemas"]["CollectionType"];
@@ -689,7 +695,7 @@ export interface components {
};
"FillerPresetResponseModel": {
"id": number;
"name": null | string;
"name": string;
"fillerKind": components["schemas"]["FillerKind"];
};
"FillWithGroupMode": "None" | "FillWithOrderedGroups" | "FillWithShuffledGroups";
@@ -697,7 +703,7 @@ export interface components {
"FixedStartTimeBehavior": "Strict" | "Flexible";
"GraphicsElementResponseModel": {
"id": number;
"name": null | string;
"name": string;
};
"GuideMode": "Normal" | "Filler";
"HardwareAccelerationKind": "None" | "Qsv" | "Nvenc" | "Vaapi" | "VideoToolbox" | "Amf" | "V4l2m2m" | "Rkmpp";
@@ -727,8 +733,8 @@ export interface components {
"durationSeconds": null | number;
};
"LanguageCodeResponseModel": {
"code": null | string;
"englishName": null | string;
"code": string;
"englishName": string;
};
"LibraryBrowseItemResponseModel": {
"id": number;
@@ -758,20 +764,20 @@ export interface components {
};
"LocalLibraryDetailResponseModel": {
"id": number;
"name": null | string;
"name": string;
"mediaKind": components["schemas"]["LibraryMediaKind"];
"isLocked": boolean;
"mediaItemCount": number;
"paths": null | Array<components["schemas"]["LocalLibraryPathResponseModel"]>;
"paths": Array<components["schemas"]["LocalLibraryPathResponseModel"]>;
};
"LocalLibraryPathResponseModel": {
"id": number;
"path": null | string;
"path": string;
"mediaItemCount": number;
};
"LocalLibraryResponseModel": {
"id": number;
"name": null | string;
"name": string;
"mediaKind": components["schemas"]["LibraryMediaKind"];
"isLocked": boolean;
};
@@ -796,19 +802,11 @@ export interface components {
"httpMinimumLogLevel": components["schemas"]["LogEventLevel"];
};
"MarathonGroupBy": "None" | "Show" | "Season" | "Artist" | "Album" | "Director";
"MediaCollectionViewModel": {
"collectionType": components["schemas"]["CollectionType"];
"MediaCollectionResponseModel": {
"id": number;
"name": null | string;
"name": string;
"collectionType": components["schemas"]["CollectionType"];
"useCustomPlaybackOrder": boolean;
"version"?: number;
"mediaItemId"?: number;
"title"?: null | string;
"subtitle"?: null | string;
"sortTitle"?: null | string;
"poster"?: null | string;
"state": components["schemas"]["MediaItemState"];
"hasMediaInfo"?: boolean;
};
"MediaItemInfoChapterResponseModel": {
"title": null | string;
@@ -915,7 +913,7 @@ export interface components {
"MultipleMode": "Count" | "CollectionSize" | "PlaylistItemSize" | "MultiEpisodeGroupSize";
"NamedIdResponseModel": {
"id": number;
"name": null | string;
"name": string;
};
"NormalizeLoudnessMode": "Off" | "LoudNorm";
"OutputFormatKind": "None" | "Mkv" | "MpegTs" | "Mp4" | "Hls" | "HlsMp4" | "Nut";
@@ -937,11 +935,11 @@ export interface components {
};
"PagedPlayoutItemsResponseModel": {
"totalCount": number;
"page": null | Array<components["schemas"]["PlayoutItemResponseModel"]>;
"page": Array<components["schemas"]["PlayoutItemResponseModel"]>;
};
"PagedPlayoutsResponseModel": {
"totalCount": number;
"page": null | Array<components["schemas"]["PlayoutListItemResponseModel"]>;
"page": Array<components["schemas"]["PlayoutListItemResponseModel"]>;
};
"PagedRerunCollectionsResponseModel": {
"totalCount": number;
@@ -958,8 +956,8 @@ export interface components {
};
"PathReplacementResponseModel": {
"id": number;
"remotePath": null | string;
"localPath": null | string;
"remotePath": string;
"localPath": string;
};
"PlaybackOrder": "None" | "Chronological" | "Random" | "Shuffle" | "ShuffleInOrder" | "MultiEpisodeShuffle" | "SeasonEpisode" | "RandomRotation" | "Marathon";
"PlaylistGroupResponseModel": {
@@ -1042,7 +1040,7 @@ export interface components {
"PlayoutBuildStatusResponseModel": {
"lastBuild": string;
"success": boolean;
"message": null | string;
"message": string;
};
"PlayoutHistoryDetailsResponseModel": {
"playbackOrder": components["schemas"]["PlaybackOrder"];
@@ -1138,7 +1136,7 @@ export interface components {
"endYear": null | number;
};
"PlexPinFlowResponseModel": {
"authUrl": null | string;
"authUrl": string;
};
"ProblemDetails": {
"type"?: null | string;
@@ -1147,18 +1145,17 @@ export interface components {
"detail"?: null | string;
"instance"?: null | string;
};
"ProgramScheduleViewModel": {
"ProgramScheduleResponseModel": {
"id": number;
"name": null | string;
"name": string;
"keepMultiPartEpisodesTogether": boolean;
"treatCollectionsAsShows": boolean;
"shuffleScheduleItems": boolean;
"randomStartPoint": boolean;
"fixedStartTimeBehavior": components["schemas"]["FixedStartTimeBehavior"];
"version": number;
};
"RemoteConnectionResponseModel": {
"address": null | string;
"address": string;
"hasApiKey": boolean;
};
"RemoteLibraryPreferenceRequest": {
@@ -1167,19 +1164,19 @@ export interface components {
};
"RemoteLibraryResponseModel": {
"id": number;
"name": null | string;
"name": string;
"mediaKind": components["schemas"]["LibraryMediaKind"];
"shouldSyncItems": boolean;
};
"RemoteMediaSourceItemResponseModel": {
"id": number;
"name": null | string;
"address": null | string;
"name": string;
"address": string;
};
"RemoteMediaSourceStateResponseModel": {
"isAuthorized": boolean;
"isLocked": boolean;
"servers": null | Array<components["schemas"]["RemoteMediaSourceItemResponseModel"]>;
"servers": Array<components["schemas"]["RemoteMediaSourceItemResponseModel"]>;
};
"ReplaceBlockRequest": {
"name": null | string;
@@ -1258,13 +1255,6 @@ export interface components {
"width": number;
"height": number;
"isCustom": boolean;
};
"ResolutionViewModel": {
"id": number;
"name": null | string;
"width": number;
"height": number;
"isCustom": boolean;
};
"SaveRemoteConnectionRequest": {
"address": null | string;
@@ -1384,16 +1374,16 @@ export interface components {
"name": string;
};
"SearchResultAllItemsResponseModel": {
"movieIds": null | Array<number>;
"showIds": null | Array<number>;
"seasonIds": null | Array<number>;
"episodeIds": null | Array<number>;
"artistIds": null | Array<number>;
"musicVideoIds": null | Array<number>;
"otherVideoIds": null | Array<number>;
"songIds": null | Array<number>;
"imageIds": null | Array<number>;
"remoteStreamIds": null | Array<number>;
"movieIds": Array<number>;
"showIds": Array<number>;
"seasonIds": Array<number>;
"episodeIds": Array<number>;
"artistIds": Array<number>;
"musicVideoIds": Array<number>;
"otherVideoIds": Array<number>;
"songIds": Array<number>;
"imageIds": Array<number>;
"remoteStreamIds": Array<number>;
};
"SearchResultGroupResponseModel": {
"totalCount": number;
@@ -1439,13 +1429,8 @@ export interface components {
};
"SmartCollectionResponseModel": {
"id": number;
"name": null | string;
"query": null | string;
};
"SmartCollectionViewModel": {
"id": number;
"name": null | string;
"query": null | string;
"name": string;
"query": string;
};
"StartType": "Dynamic" | "Fixed";
"StreamingMode": "TransportStream" | "HttpLiveStreamingDirect" | "HttpLiveStreamingSegmenter" | "TransportStreamHybrid";
@@ -1749,6 +1734,16 @@ export interface components {
"isValid": boolean;
"messages": Array<string>;
"json": string;
};
"ValidationProblemDetails": {
"type"?: null | string;
"title"?: null | string;
"status"?: null | number;
"detail"?: null | string;
"instance"?: null | string;
"errors"?: {
[key: string]: Array<string>;
};
};
"VideoScanKind": "Unknown" | "Progressive" | "Interlaced";
"WatermarkFullResponseModel": {
@@ -1773,7 +1768,7 @@ export interface components {
"WatermarkLocation": "BottomRight" | "BottomLeft" | "TopRight" | "TopLeft" | "TopMiddle" | "RightMiddle" | "BottomMiddle" | "LeftMiddle" | "MiddleCenter";
"WatermarkResponseModel": {
"id": number;
"name": null | string;
"name": string;
};
"WatermarkSize": "Scaled" | "ActualSize";
"XmltvBlockBehavior": "SplitTimeEvenly" | "UseActualTimes";