feat(api): composite create-channel endpoint (#63)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m58s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped

This commit is contained in:
2026-07-06 20:27:02 +02:00
parent 16674ba80e
commit 6857a0d191
11 changed files with 1594 additions and 0 deletions
@@ -79,6 +79,25 @@ public class ChannelController(ChannelWriter<IBackgroundServiceRequest> workerCh
});
}
[HttpPost("/api/channels/from-lineup", Name = "CreateChannelFromLineup")]
[Tags("Channels")]
[EndpointSummary("Create a channel from a library lineup")]
[EndpointDescription(
"Atomically creates the channel, generated lineup collection, program schedule, schedule items, " +
"and classic playout. Template defaults are stamped at create time; advanced overrides win.")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(CreateChannelFromLineupResponseModel), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> CreateFromLineup(
[Required] [FromBody] CreateChannelFromLineupRequest request,
CancellationToken cancellationToken)
{
Either<BaseError, CreateChannelFromLineupResponseModel> result =
await mediator.Send(request.ToCommand(), cancellationToken);
return result.ToCreatedResult(response => $"/api/channels/{response.ChannelId}", response => response);
}
[HttpPut("/api/channels/{id:int}")]
[Tags("Channels")]
[EndpointSummary("Update a channel")]
@@ -0,0 +1,111 @@
#nullable enable
using ErsatzTV.Application.Artworks;
using ErsatzTV.Application.Channels;
using ErsatzTV.Core.Api.LibraryBrowse;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Controllers.Api.Requests;
public record CreateChannelFromLineupRequest(
string Name,
string Number,
string Group,
string Categories,
ArtworkContentTypeModel Logo,
bool IsEnabled,
bool ShowInEpg,
int TemplateId,
CreateChannelFromLineupAdvancedOptionsRequest? Advanced,
List<CreateChannelFromLineupItemRequest> Lineup)
{
public CreateChannelFromLineup ToCommand() =>
new(
Name,
Number,
Group,
Categories,
Logo,
IsEnabled,
ShowInEpg,
TemplateId,
Advanced?.ToCommand() ?? new CreateChannelFromLineupAdvancedOptions(),
Lineup.Map(i => i.ToCommand()).ToList());
}
public record CreateChannelFromLineupAdvancedOptionsRequest(
PlaybackOrder? PlaybackOrder = null,
int? FFmpegProfileId = null,
int? WatermarkId = null,
int? FallbackFillerId = null,
int? PreRollFillerId = null,
int? MidRollFillerId = null,
int? PostRollFillerId = null,
ChannelStreamSelectorMode? StreamSelectorMode = null,
string? StreamSelector = null,
string? PreferredAudioLanguageCode = null,
string? PreferredAudioTitle = null,
ChannelPlayoutSource? PlayoutSource = null,
ChannelPlayoutMode? PlayoutMode = null,
StreamingMode? StreamingMode = null,
string? PreferredSubtitleLanguageCode = null,
ChannelSubtitleMode? SubtitleMode = null,
ChannelMusicVideoCreditsMode? MusicVideoCreditsMode = null,
string? MusicVideoCreditsTemplate = null,
ChannelSongVideoMode? SongVideoMode = null,
ChannelTranscodeMode? TranscodeMode = null,
ChannelIdleBehavior? IdleBehavior = null,
bool? ShuffleScheduleItems = null,
bool? RandomStartPoint = null,
FixedStartTimeBehavior? FixedStartTimeBehavior = null)
{
public CreateChannelFromLineupAdvancedOptions ToCommand() =>
new(
PlaybackOrder,
FFmpegProfileId,
WatermarkId,
FallbackFillerId,
PreRollFillerId,
MidRollFillerId,
PostRollFillerId,
StreamSelectorMode,
StreamSelector,
PreferredAudioLanguageCode,
PreferredAudioTitle,
PlayoutSource,
PlayoutMode,
StreamingMode,
PreferredSubtitleLanguageCode,
SubtitleMode,
MusicVideoCreditsMode,
MusicVideoCreditsTemplate,
SongVideoMode,
TranscodeMode,
IdleBehavior,
ShuffleScheduleItems,
RandomStartPoint,
FixedStartTimeBehavior);
}
public record CreateChannelFromLineupItemRequest(
LibraryBrowseMediaType MediaType,
CollectionType CollectionType,
int? CollectionId,
int? MultiCollectionId,
int? SmartCollectionId,
int? RerunCollectionId,
int? MediaItemId,
int? PlaylistId)
{
public CreateChannelFromLineupItem ToCommand() =>
new(
MediaType,
CollectionType,
CollectionId,
MultiCollectionId,
SmartCollectionId,
RerunCollectionId,
MediaItemId,
PlaylistId);
}
+444
View File
@@ -531,6 +531,103 @@
}
}
},
"/api/channels/from-lineup": {
"post": {
"tags": [
"Channels"
],
"summary": "Create a channel from a library lineup",
"description": "Atomically creates the channel, generated lineup collection, program schedule, schedule items, and classic playout. Template defaults are stamped at create time; advanced overrides win.",
"operationId": "CreateChannelFromLineup",
"requestBody": {
"content": {
"application/json-patch+json": {
"schema": {
"$ref": "#/components/schemas/CreateChannelFromLineupRequest"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateChannelFromLineupRequest"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/CreateChannelFromLineupRequest"
}
},
"application/*+json": {
"schema": {
"$ref": "#/components/schemas/CreateChannelFromLineupRequest"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "Created",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/CreateChannelFromLineupResponseModel"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateChannelFromLineupResponseModel"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/CreateChannelFromLineupResponseModel"
}
}
}
},
"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/channels/bulk/renumber": {
"post": {
"tags": [
@@ -5187,6 +5284,353 @@
}
}
},
"CreateChannelFromLineupAdvancedOptionsRequest": {
"type": "object",
"properties": {
"playbackOrder": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/PlaybackOrder"
}
]
},
"fFmpegProfileId": {
"type": [
"null",
"integer"
],
"format": "int32"
},
"watermarkId": {
"type": [
"null",
"integer"
],
"format": "int32"
},
"fallbackFillerId": {
"type": [
"null",
"integer"
],
"format": "int32"
},
"preRollFillerId": {
"type": [
"null",
"integer"
],
"format": "int32"
},
"midRollFillerId": {
"type": [
"null",
"integer"
],
"format": "int32"
},
"postRollFillerId": {
"type": [
"null",
"integer"
],
"format": "int32"
},
"streamSelectorMode": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ChannelStreamSelectorMode"
}
]
},
"streamSelector": {
"type": [
"null",
"string"
]
},
"preferredAudioLanguageCode": {
"type": [
"null",
"string"
]
},
"preferredAudioTitle": {
"type": [
"null",
"string"
]
},
"playoutSource": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ChannelPlayoutSource"
}
]
},
"playoutMode": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ChannelPlayoutMode"
}
]
},
"streamingMode": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/StreamingMode"
}
]
},
"preferredSubtitleLanguageCode": {
"type": [
"null",
"string"
]
},
"subtitleMode": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ChannelSubtitleMode"
}
]
},
"musicVideoCreditsMode": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ChannelMusicVideoCreditsMode"
}
]
},
"musicVideoCreditsTemplate": {
"type": [
"null",
"string"
]
},
"songVideoMode": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ChannelSongVideoMode"
}
]
},
"transcodeMode": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ChannelTranscodeMode"
}
]
},
"idleBehavior": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ChannelIdleBehavior"
}
]
},
"shuffleScheduleItems": {
"type": [
"null",
"boolean"
]
},
"randomStartPoint": {
"type": [
"null",
"boolean"
]
},
"fixedStartTimeBehavior": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/FixedStartTimeBehavior"
}
]
}
}
},
"CreateChannelFromLineupItemRequest": {
"required": [
"mediaType",
"collectionType",
"collectionId",
"multiCollectionId",
"smartCollectionId",
"rerunCollectionId",
"mediaItemId",
"playlistId"
],
"type": "object",
"properties": {
"mediaType": {
"$ref": "#/components/schemas/LibraryBrowseMediaType"
},
"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"
}
}
},
"CreateChannelFromLineupRequest": {
"required": [
"name",
"number",
"group",
"categories",
"logo",
"isEnabled",
"showInEpg",
"templateId",
"advanced",
"lineup"
],
"type": "object",
"properties": {
"name": {
"type": "string"
},
"number": {
"type": "string"
},
"group": {
"type": "string"
},
"categories": {
"type": "string"
},
"logo": {
"$ref": "#/components/schemas/ArtworkContentTypeModel"
},
"isEnabled": {
"type": "boolean"
},
"showInEpg": {
"type": "boolean"
},
"templateId": {
"type": "integer",
"format": "int32"
},
"advanced": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/CreateChannelFromLineupAdvancedOptionsRequest"
}
]
},
"lineup": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CreateChannelFromLineupItemRequest"
}
}
}
},
"CreateChannelFromLineupResponseModel": {
"required": [
"channelId",
"collectionId",
"programScheduleId",
"playoutId"
],
"type": "object",
"properties": {
"channelId": {
"type": "integer",
"format": "int32"
},
"collectionId": {
"type": "integer",
"format": "int32"
},
"programScheduleId": {
"type": "integer",
"format": "int32"
},
"playoutId": {
"type": "integer",
"format": "int32"
}
}
},
"CreateChannelRequest": {
"required": [
"name",