Files
ersatztv/ErsatzTV/Controllers/Api/Requests/CreateChannelFromLineupRequest.cs
T
timothyandClaude Fable 5 cf36c30997
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m23s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m54s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
fix(api): rework from-lineup to generated playlist design + review fixes (#63)
Adversarial review found the previous multi-flood design non-viable:
PlayoutModeSchedulerFlood never yields to a following Dynamic-start
schedule item, so only the first item ever played, and grouping
media items into a Collection silently dropped the requested order.

Redesign:
- Single-item lineup: one ProgramScheduleItemFlood referencing the
  target directly (media item / collection / smart / multi / rerun /
  playlist); no generated collection or playlist. Response PlaylistId
  is null.
- Multi-item lineup (>= 2): one generated IsSystem Playlist in a
  get-or-created IsSystem PlaylistGroup ("Channel Lineups"), one
  PlaylistItem per entry in lineup order with PlayAll=true, referenced
  by a single Flood schedule item. Rerun collections and playlists are
  rejected (422) in multi lineups (PlaylistItem/CollectionKey lack the
  fields to enumerate them).

Review fixes:
- Normalize + strict-validate MediaType<->CollectionType pairs once up
  front (422 on mismatch / wrong id / not exactly one id).
- Reject MultiCollection with non-Shuffle order (mirrors
  PlayoutModeMustBeValid), Mirror playout source, all via 422.
- OnDemand parity: queue TimeShiftOnDemandPlayout post-commit.
- De-collide generated ProgramSchedule and Playlist names against their
  unique indexes instead of leaking a UNIQUE-constraint DbUpdateException.
- Generic 422 on save failure + ILogger; AnyAsync existence checks;
  Either/Validation unwrap via Match; XML doc on request DTO + endpoint.
- Response model: ChannelId, PlaylistId (nullable), ProgramScheduleId,
  PlayoutId (CollectionId removed). Regenerated OpenAPI v1.json + v1.d.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:09:00 +02:00

118 lines
3.7 KiB
C#

#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;
/// <summary>
/// Composite request to create a channel, its generated schedule/playlist and a classic playout in one call.
/// Template defaults are stamped at create time; any value set in <see cref="Advanced" /> overrides the template.
/// A single-item lineup references its target directly; a multi-item lineup is played in order via a generated
/// system playlist.
/// </summary>
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);
}