Add the id-based reconcile tests to ReplaceProgramScheduleItemsReconcileTests: reorder moves state with the logical item (the non-vacuous core — proven to fail under forced-positional), insert-in-middle, delete-unreferenced, unknown-id→422, duplicate-id→422, and stale-version+unknown-id→412 (412 precedes 422, §7c). The GET→map→PUT lossless round-trip now round-trips r.Id so it exercises id-mode. Threads the new int? Id through all command/wire construction sites in tests. Docs: api-conventions §7c (stable child identity + the deliberate #2-#5 positional asymmetry) and a decisions.md entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
453 lines
21 KiB
C#
453 lines
21 KiB
C#
using System.Threading.Channels;
|
|
using ErsatzTV.Application;
|
|
using ErsatzTV.Application.ProgramSchedules;
|
|
using ErsatzTV.Core;
|
|
using ErsatzTV.Core.Api.Scheduling;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Core.Domain.Filler;
|
|
using ErsatzTV.Core.Interfaces.Repositories;
|
|
using ErsatzTV.Core.Scheduling;
|
|
using ErsatzTV.Infrastructure.Data;
|
|
using ErsatzTV.Tests.Support;
|
|
using LanguageExt;
|
|
using MediatR;
|
|
using NSubstitute;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
|
|
namespace ErsatzTV.Tests.Application.ProgramSchedules;
|
|
|
|
/// <summary>
|
|
/// Release gate for issue #126: proves the flat <see cref="ScheduleItemResponseModel" /> produced by
|
|
/// the GET endpoint carries enough information to reconstruct an identical PUT (ReplaceProgramScheduleItems)
|
|
/// with no loss — a GET → map → PUT → GET fixed point across every subtype and field family. Also documents
|
|
/// the deliberate <c>EnforceProperties</c> normalization (shuffle → Dynamic/One/None).
|
|
/// </summary>
|
|
[TestFixture]
|
|
public class ScheduleItemResponseRoundTripTests
|
|
{
|
|
private InMemoryTvContext _db = null!;
|
|
private ChannelWriter<IBackgroundServiceRequest> _worker = null!;
|
|
|
|
[SetUp]
|
|
public async Task SetUp()
|
|
{
|
|
_db = await InMemoryTvContext.CreateAsync();
|
|
_worker = System.Threading.Channels.Channel.CreateUnbounded<IBackgroundServiceRequest>().Writer;
|
|
}
|
|
|
|
[TearDown]
|
|
public async Task TearDown() => await _db.DisposeAsync();
|
|
|
|
[Test]
|
|
public async Task Get_Map_Replace_Get_Should_Be_Lossless_Across_All_Subtypes()
|
|
{
|
|
int scheduleId = await SeedScheduleAndReferences(shuffleScheduleItems: false);
|
|
|
|
var replaceHandler = new ReplaceProgramScheduleItemsHandler(_db.Factory, _worker);
|
|
|
|
// Arrange: establish the initial items through the real write path.
|
|
Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>> seeded =
|
|
await replaceHandler.Handle(new ReplaceProgramScheduleItems(scheduleId, BuildSeedItems()), CancellationToken.None);
|
|
seeded.IsRight.ShouldBeTrue(seeded.LeftToSeq().HeadOrNone().Match(e => e.Value, () => "unknown"));
|
|
|
|
// Act: GET (with durations) → flat DTO envelope A
|
|
ScheduleItemsResponseModel envelopeA = await GetItemsEnvelope(scheduleId);
|
|
envelopeA.Items.Count.ShouldBe(6);
|
|
|
|
// Map every response item back into a Replace command exactly as the request mapping would, then PUT.
|
|
// Reconstruct in the item's own (stable) Index order, mirroring how the SPA re-submits the ordered list.
|
|
List<ReplaceProgramScheduleItem> reconstructed = envelopeA.Items
|
|
.OrderBy(item => item.Index)
|
|
.Select((item, index) => ToReplaceCommand(item, index))
|
|
.ToList();
|
|
Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>> replaced =
|
|
await replaceHandler.Handle(new ReplaceProgramScheduleItems(scheduleId, reconstructed), CancellationToken.None);
|
|
replaced.IsRight.ShouldBeTrue(replaced.LeftToSeq().HeadOrNone().Match(e => e.Value, () => "unknown"));
|
|
|
|
// GET again → envelope B; A and B must be semantically identical (ignoring regenerated row ids).
|
|
ScheduleItemsResponseModel envelopeB = await GetItemsEnvelope(scheduleId);
|
|
|
|
envelopeB.Items.Count.ShouldBe(envelopeA.Items.Count);
|
|
envelopeB.TotalDurationEstimate.ShouldBe(envelopeA.TotalDurationEstimate);
|
|
|
|
// The GET does not guarantee row order, so pair items up by their (stable) Index before comparing.
|
|
List<ScheduleItemResponseModel> orderedA = envelopeA.Items.OrderBy(i => i.Index).ToList();
|
|
List<ScheduleItemResponseModel> orderedB = envelopeB.Items.OrderBy(i => i.Index).ToList();
|
|
for (var i = 0; i < orderedA.Count; i++)
|
|
{
|
|
AssertSemanticallyEqual(orderedA[i], orderedB[i]);
|
|
}
|
|
|
|
// Spot-check that the display/hydration fields actually populated (proves the mapper is non-vacuous).
|
|
ScheduleItemResponseModel multiple = envelopeA.Items.Single(i => i.PlayoutMode == PlayoutMode.Multiple);
|
|
multiple.CollectionName.ShouldBe("Prime Collection");
|
|
multiple.MultipleMode.ShouldBe(MultipleMode.Count);
|
|
multiple.MultipleCount.ShouldBe("2 + 1");
|
|
multiple.FillWithGroupMode.ShouldBe(FillWithGroupMode.FillWithShuffledGroups);
|
|
multiple.PreRollFillerId.ShouldBe(1);
|
|
multiple.PreRollFillerName.ShouldBe("PreRoll");
|
|
multiple.FallbackFillerId.ShouldBe(5);
|
|
multiple.WatermarkIds.ShouldBe([1, 2]);
|
|
multiple.Watermarks.Select(w => w.Name).ShouldBe(["WM One", "WM Two"]);
|
|
multiple.GraphicsElementIds.ShouldBe([1, 2]);
|
|
multiple.GraphicsElements.Select(g => g.Id).ShouldBe([1, 2]);
|
|
multiple.GraphicsElements.Select(g => g.Name).ShouldAllBe(n => !string.IsNullOrWhiteSpace(n));
|
|
|
|
ScheduleItemResponseModel one = envelopeA.Items.Single(i =>
|
|
i.PlayoutMode == PlayoutMode.One && i.CollectionType == CollectionType.Collection);
|
|
one.StartType.ShouldBe(StartType.Fixed);
|
|
one.StartTime.ShouldBe(TimeSpan.FromHours(20));
|
|
one.FixedStartTimeBehavior.ShouldBe(FixedStartTimeBehavior.Flexible);
|
|
one.CustomTitle.ShouldBe("News Hour");
|
|
one.PreferredAudioLanguageCode.ShouldBe("eng");
|
|
one.PreferredAudioTitle.ShouldBe("Director Commentary");
|
|
one.PreferredSubtitleLanguageCode.ShouldBe("fra");
|
|
one.SubtitleMode.ShouldBe(ChannelSubtitleMode.Any);
|
|
|
|
ScheduleItemResponseModel duration = envelopeA.Items.Single(i => i.PlayoutMode == PlayoutMode.Duration);
|
|
duration.SmartCollectionName.ShouldBe("Smart Picks");
|
|
duration.PlayoutDuration.ShouldBe(TimeSpan.FromMinutes(45));
|
|
duration.TailMode.ShouldBe(TailMode.Filler);
|
|
duration.TailFillerId.ShouldBe(4);
|
|
duration.PlaybackOrder.ShouldBe(PlaybackOrder.Marathon);
|
|
duration.MarathonGroupBy.ShouldBe(MarathonGroupBy.Show);
|
|
duration.MarathonBatchSize.ShouldBe(4);
|
|
// Seeded with DiscardToFillAttempts = 5, but the write path's FixDiscardToFillAttempts zeroes
|
|
// it for any order other than Random/Shuffle (Marathon here) — a deliberate server-side
|
|
// normalization. Pinning it here keeps the lossless round-trip honest: A already holds 0, so
|
|
// the re-submit maps 0 → 0 and B stays equal.
|
|
duration.DiscardToFillAttempts.ShouldBe(0);
|
|
|
|
ScheduleItemResponseModel flood = envelopeA.Items.Single(i => i.PlayoutMode == PlayoutMode.Flood);
|
|
flood.PlaylistName.ShouldBe("My Playlist");
|
|
flood.PlaylistGroupId.ShouldBe(9);
|
|
flood.MarathonShuffleGroups.ShouldBeTrue();
|
|
|
|
ScheduleItemResponseModel rerun = envelopeA.Items.Single(i => i.CollectionType == CollectionType.RerunFirstRun);
|
|
rerun.RerunCollectionName.ShouldBe("Rerun Bin");
|
|
|
|
ScheduleItemResponseModel search = envelopeA.Items.Single(i => i.CollectionType == CollectionType.SearchQuery);
|
|
search.SearchQuery.ShouldBe("genre:comedy");
|
|
search.Name.ShouldBe("Comedy Search");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Get_Should_Apply_EnforceProperties_Normalization_When_Shuffle_Enabled()
|
|
{
|
|
int scheduleId = await SeedScheduleAndReferences(shuffleScheduleItems: true);
|
|
|
|
var replaceHandler = new ReplaceProgramScheduleItemsHandler(_db.Factory, _worker);
|
|
Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>> seeded = await replaceHandler.Handle(
|
|
new ReplaceProgramScheduleItems(
|
|
scheduleId,
|
|
[
|
|
// Fixed-start Flood item on a plain collection.
|
|
MakeReplace(0, PlayoutMode.Flood, CollectionType.Collection) with
|
|
{
|
|
StartTime = TimeSpan.FromHours(6),
|
|
CollectionId = 1,
|
|
PlaybackOrder = PlaybackOrder.Chronological
|
|
},
|
|
// Playlist item carrying a playback order that must be normalized away.
|
|
MakeReplace(1, PlayoutMode.One, CollectionType.Playlist) with
|
|
{
|
|
PlaylistId = 1,
|
|
PlaybackOrder = PlaybackOrder.Chronological
|
|
}
|
|
]),
|
|
CancellationToken.None);
|
|
seeded.IsRight.ShouldBeTrue();
|
|
|
|
List<ScheduleItemResponseModel> items = (await GetItemsEnvelope(scheduleId)).Items;
|
|
|
|
ScheduleItemResponseModel floodItem = items.Single(i => i.CollectionType == CollectionType.Collection);
|
|
// Flood → One and Fixed → Dynamic are the documented, deliberate rewrites when ShuffleScheduleItems is on.
|
|
floodItem.PlayoutMode.ShouldBe(PlayoutMode.One);
|
|
floodItem.StartType.ShouldBe(StartType.Dynamic);
|
|
|
|
ScheduleItemResponseModel playlistItem = items.Single(i => i.CollectionType == CollectionType.Playlist);
|
|
playlistItem.PlaybackOrder.ShouldBe(PlaybackOrder.None);
|
|
}
|
|
|
|
private async Task<ScheduleItemsResponseModel> GetItemsEnvelope(int scheduleId)
|
|
{
|
|
var itemsHandler = new GetProgramScheduleItemsHandler(_db.Factory);
|
|
IMediator mediator = Substitute.For<IMediator>();
|
|
mediator.Send(Arg.Any<GetProgramScheduleItems>(), Arg.Any<CancellationToken>())
|
|
.Returns(ci => itemsHandler.Handle((GetProgramScheduleItems)ci[0], (CancellationToken)ci[1]));
|
|
|
|
IMediaCollectionRepository repo = Substitute.For<IMediaCollectionRepository>();
|
|
repo.GetItems(Arg.Any<int>()).Returns(new List<MediaItem>());
|
|
|
|
var withDurations = new GetProgramScheduleItemsWithDurationsHandler(mediator, repo);
|
|
ProgramScheduleItemsWithDurationViewModel vm =
|
|
await withDurations.Handle(new GetProgramScheduleItemsWithDurations(scheduleId), CancellationToken.None);
|
|
return ScheduleItemResponseMapper.ProjectToResponseModel(vm);
|
|
}
|
|
|
|
private static List<ReplaceProgramScheduleItem> BuildSeedItems() =>
|
|
[
|
|
// 0: Fixed-start One on a plain collection, full preferred-audio/subtitle + custom title.
|
|
MakeReplace(0, PlayoutMode.One, CollectionType.Collection) with
|
|
{
|
|
StartType = StartType.Fixed,
|
|
StartTime = TimeSpan.FromHours(20),
|
|
FixedStartTimeBehavior = FixedStartTimeBehavior.Flexible,
|
|
CollectionId = 1,
|
|
PlaybackOrder = PlaybackOrder.Chronological,
|
|
CustomTitle = "News Hour",
|
|
PreferredAudioLanguageCode = "eng",
|
|
PreferredAudioTitle = "Director Commentary",
|
|
PreferredSubtitleLanguageCode = "fra",
|
|
SubtitleMode = ChannelSubtitleMode.Any
|
|
},
|
|
// 1: Multiple (Count, expression string) on a different collection, group mode + all 5 fillers + 2 wm + 2 ge.
|
|
MakeReplace(1, PlayoutMode.Multiple, CollectionType.Collection) with
|
|
{
|
|
CollectionId = 2,
|
|
PlaybackOrder = PlaybackOrder.Shuffle,
|
|
FillWithGroupMode = FillWithGroupMode.FillWithShuffledGroups,
|
|
MultipleMode = MultipleMode.Count,
|
|
MultipleCount = "2 + 1",
|
|
PreRollFillerId = 1,
|
|
MidRollFillerId = 2,
|
|
PostRollFillerId = 3,
|
|
TailFillerId = 4,
|
|
FallbackFillerId = 5,
|
|
WatermarkIds = [1, 2],
|
|
GraphicsElementIds = [1, 2]
|
|
},
|
|
// 2: Duration on a smart collection, Marathon order + tail filler.
|
|
MakeReplace(2, PlayoutMode.Duration, CollectionType.SmartCollection) with
|
|
{
|
|
SmartCollectionId = 1,
|
|
PlayoutDuration = TimeSpan.FromMinutes(45),
|
|
TailMode = TailMode.Filler,
|
|
TailFillerId = 4,
|
|
DiscardToFillAttempts = 5,
|
|
PlaybackOrder = PlaybackOrder.Marathon,
|
|
MarathonGroupBy = MarathonGroupBy.Show,
|
|
MarathonShuffleGroups = true,
|
|
MarathonShuffleItems = true,
|
|
MarathonBatchSize = 4
|
|
},
|
|
// 3: Flood on a playlist (playback order None; MarathonShuffleGroups doubles as shuffle-playlist-items).
|
|
MakeReplace(3, PlayoutMode.Flood, CollectionType.Playlist) with
|
|
{
|
|
PlaylistId = 1,
|
|
PlaybackOrder = PlaybackOrder.None,
|
|
MarathonShuffleGroups = true
|
|
},
|
|
// 4: Rerun-first-run One item.
|
|
MakeReplace(4, PlayoutMode.One, CollectionType.RerunFirstRun) with
|
|
{
|
|
RerunCollectionId = 1,
|
|
PlaybackOrder = PlaybackOrder.None
|
|
},
|
|
// 5: SearchQuery One item.
|
|
MakeReplace(5, PlayoutMode.One, CollectionType.SearchQuery) with
|
|
{
|
|
SearchTitle = "Comedy Search",
|
|
SearchQuery = "genre:comedy",
|
|
PlaybackOrder = PlaybackOrder.Shuffle
|
|
}
|
|
];
|
|
|
|
private static ReplaceProgramScheduleItem MakeReplace(int index, PlayoutMode playoutMode, CollectionType collectionType) =>
|
|
new(
|
|
null,
|
|
index,
|
|
StartType.Dynamic,
|
|
StartTime: null,
|
|
FixedStartTimeBehavior: null,
|
|
playoutMode,
|
|
collectionType,
|
|
CollectionId: null,
|
|
MultiCollectionId: null,
|
|
SmartCollectionId: null,
|
|
RerunCollectionId: null,
|
|
MediaItemId: null,
|
|
PlaylistId: null,
|
|
SearchTitle: null,
|
|
SearchQuery: null,
|
|
PlaybackOrder: PlaybackOrder.Shuffle,
|
|
MarathonGroupBy: MarathonGroupBy.None,
|
|
MarathonShuffleGroups: false,
|
|
MarathonShuffleItems: false,
|
|
MarathonBatchSize: null,
|
|
FillWithGroupMode: FillWithGroupMode.None,
|
|
MultipleMode: MultipleMode.Count,
|
|
MultipleCount: null,
|
|
PlayoutDuration: null,
|
|
TailMode: TailMode.None,
|
|
DiscardToFillAttempts: null,
|
|
CustomTitle: null,
|
|
GuideMode: GuideMode.Normal,
|
|
PreRollFillerId: null,
|
|
MidRollFillerId: null,
|
|
PostRollFillerId: null,
|
|
TailFillerId: null,
|
|
FallbackFillerId: null,
|
|
WatermarkIds: [],
|
|
GraphicsElementIds: [],
|
|
PreferredAudioLanguageCode: null,
|
|
PreferredAudioTitle: null,
|
|
PreferredSubtitleLanguageCode: null,
|
|
SubtitleMode: null);
|
|
|
|
// Mirrors the ScheduleItemRequest → ReplaceProgramScheduleItem controller mapping, but sourced from a
|
|
// response DTO (the SPA does this same field-for-field copy in TypeScript before a PUT).
|
|
private static ReplaceProgramScheduleItem ToReplaceCommand(ScheduleItemResponseModel r, int index) =>
|
|
new(
|
|
r.Id,
|
|
index,
|
|
r.StartType,
|
|
r.StartTime,
|
|
r.FixedStartTimeBehavior,
|
|
r.PlayoutMode,
|
|
r.CollectionType,
|
|
r.CollectionId,
|
|
r.MultiCollectionId,
|
|
r.SmartCollectionId,
|
|
r.RerunCollectionId,
|
|
r.MediaItemId,
|
|
r.PlaylistId,
|
|
r.SearchTitle,
|
|
r.SearchQuery,
|
|
r.PlaybackOrder,
|
|
r.MarathonGroupBy,
|
|
r.MarathonShuffleGroups,
|
|
r.MarathonShuffleItems,
|
|
r.MarathonBatchSize,
|
|
r.FillWithGroupMode,
|
|
r.MultipleMode ?? MultipleMode.Count,
|
|
r.MultipleCount,
|
|
r.PlayoutDuration,
|
|
r.TailMode ?? TailMode.None,
|
|
r.DiscardToFillAttempts,
|
|
r.CustomTitle,
|
|
r.GuideMode,
|
|
r.PreRollFillerId,
|
|
r.MidRollFillerId,
|
|
r.PostRollFillerId,
|
|
r.TailFillerId,
|
|
r.FallbackFillerId,
|
|
r.WatermarkIds,
|
|
r.GraphicsElementIds,
|
|
r.PreferredAudioLanguageCode,
|
|
r.PreferredAudioTitle,
|
|
r.PreferredSubtitleLanguageCode,
|
|
r.SubtitleMode);
|
|
|
|
private static void AssertSemanticallyEqual(ScheduleItemResponseModel a, ScheduleItemResponseModel b)
|
|
{
|
|
b.Index.ShouldBe(a.Index);
|
|
b.StartType.ShouldBe(a.StartType);
|
|
b.StartTime.ShouldBe(a.StartTime);
|
|
b.FixedStartTimeBehavior.ShouldBe(a.FixedStartTimeBehavior);
|
|
b.PlayoutMode.ShouldBe(a.PlayoutMode);
|
|
b.CollectionType.ShouldBe(a.CollectionType);
|
|
b.CollectionId.ShouldBe(a.CollectionId);
|
|
b.MultiCollectionId.ShouldBe(a.MultiCollectionId);
|
|
b.SmartCollectionId.ShouldBe(a.SmartCollectionId);
|
|
b.RerunCollectionId.ShouldBe(a.RerunCollectionId);
|
|
b.MediaItemId.ShouldBe(a.MediaItemId);
|
|
b.PlaylistId.ShouldBe(a.PlaylistId);
|
|
b.SearchTitle.ShouldBe(a.SearchTitle);
|
|
b.SearchQuery.ShouldBe(a.SearchQuery);
|
|
b.PlaybackOrder.ShouldBe(a.PlaybackOrder);
|
|
b.MarathonGroupBy.ShouldBe(a.MarathonGroupBy);
|
|
b.MarathonShuffleGroups.ShouldBe(a.MarathonShuffleGroups);
|
|
b.MarathonShuffleItems.ShouldBe(a.MarathonShuffleItems);
|
|
b.MarathonBatchSize.ShouldBe(a.MarathonBatchSize);
|
|
b.FillWithGroupMode.ShouldBe(a.FillWithGroupMode);
|
|
b.MultipleMode.ShouldBe(a.MultipleMode);
|
|
b.MultipleCount.ShouldBe(a.MultipleCount);
|
|
b.PlayoutDuration.ShouldBe(a.PlayoutDuration);
|
|
b.TailMode.ShouldBe(a.TailMode);
|
|
b.DiscardToFillAttempts.ShouldBe(a.DiscardToFillAttempts);
|
|
b.CustomTitle.ShouldBe(a.CustomTitle);
|
|
b.GuideMode.ShouldBe(a.GuideMode);
|
|
b.PreRollFillerId.ShouldBe(a.PreRollFillerId);
|
|
b.MidRollFillerId.ShouldBe(a.MidRollFillerId);
|
|
b.PostRollFillerId.ShouldBe(a.PostRollFillerId);
|
|
b.TailFillerId.ShouldBe(a.TailFillerId);
|
|
b.FallbackFillerId.ShouldBe(a.FallbackFillerId);
|
|
b.WatermarkIds.ShouldBe(a.WatermarkIds);
|
|
b.GraphicsElementIds.ShouldBe(a.GraphicsElementIds);
|
|
b.PreferredAudioLanguageCode.ShouldBe(a.PreferredAudioLanguageCode);
|
|
b.PreferredAudioTitle.ShouldBe(a.PreferredAudioTitle);
|
|
b.PreferredSubtitleLanguageCode.ShouldBe(a.PreferredSubtitleLanguageCode);
|
|
b.SubtitleMode.ShouldBe(a.SubtitleMode);
|
|
b.CollectionName.ShouldBe(a.CollectionName);
|
|
b.MultiCollectionName.ShouldBe(a.MultiCollectionName);
|
|
b.SmartCollectionName.ShouldBe(a.SmartCollectionName);
|
|
b.RerunCollectionName.ShouldBe(a.RerunCollectionName);
|
|
b.PlaylistName.ShouldBe(a.PlaylistName);
|
|
b.PlaylistGroupId.ShouldBe(a.PlaylistGroupId);
|
|
b.MediaItemName.ShouldBe(a.MediaItemName);
|
|
b.PreRollFillerName.ShouldBe(a.PreRollFillerName);
|
|
b.MidRollFillerName.ShouldBe(a.MidRollFillerName);
|
|
b.PostRollFillerName.ShouldBe(a.PostRollFillerName);
|
|
b.TailFillerName.ShouldBe(a.TailFillerName);
|
|
b.FallbackFillerName.ShouldBe(a.FallbackFillerName);
|
|
b.Watermarks.Select(w => (w.Id, w.Name)).ShouldBe(a.Watermarks.Select(w => (w.Id, w.Name)));
|
|
b.GraphicsElements.Select(g => (g.Id, g.Name)).ShouldBe(a.GraphicsElements.Select(g => (g.Id, g.Name)));
|
|
b.Name.ShouldBe(a.Name);
|
|
b.DurationEstimate.ShouldBe(a.DurationEstimate);
|
|
}
|
|
|
|
private async Task<int> SeedScheduleAndReferences(bool shuffleScheduleItems)
|
|
{
|
|
await using TvContext context = _db.CreateContext();
|
|
|
|
context.Collections.Add(new Collection { Id = 1, Name = "First Collection" });
|
|
context.Collections.Add(new Collection { Id = 2, Name = "Prime Collection" });
|
|
context.SmartCollections.Add(new SmartCollection { Id = 1, Name = "Smart Picks", Query = "*" });
|
|
context.RerunCollections.Add(new RerunCollection
|
|
{
|
|
Id = 1,
|
|
Name = "Rerun Bin",
|
|
CollectionType = CollectionType.Collection
|
|
});
|
|
|
|
context.PlaylistGroups.Add(new PlaylistGroup { Id = 9, Name = "Group" });
|
|
context.Playlists.Add(new Playlist { Id = 1, PlaylistGroupId = 9, Name = "My Playlist" });
|
|
|
|
context.ChannelWatermarks.Add(new ChannelWatermark { Id = 1, Name = "WM One" });
|
|
context.ChannelWatermarks.Add(new ChannelWatermark { Id = 2, Name = "WM Two" });
|
|
context.GraphicsElements.Add(new GraphicsElement { Id = 1, Name = "GE One", Path = "/ge/1", Kind = GraphicsElementKind.Image });
|
|
context.GraphicsElements.Add(new GraphicsElement { Id = 2, Name = "GE Two", Path = "/ge/2", Kind = GraphicsElementKind.Image });
|
|
|
|
AddFiller(context, 1, "PreRoll", FillerKind.PreRoll);
|
|
AddFiller(context, 2, "MidRoll", FillerKind.MidRoll);
|
|
AddFiller(context, 3, "PostRoll", FillerKind.PostRoll);
|
|
AddFiller(context, 4, "Tail", FillerKind.Tail);
|
|
AddFiller(context, 5, "Fallback", FillerKind.Fallback);
|
|
|
|
var schedule = new ProgramSchedule
|
|
{
|
|
Name = "Round Trip",
|
|
ShuffleScheduleItems = shuffleScheduleItems,
|
|
Items = [],
|
|
Playouts = [],
|
|
ProgramScheduleAlternates = []
|
|
};
|
|
context.ProgramSchedules.Add(schedule);
|
|
await context.SaveChangesAsync();
|
|
return schedule.Id;
|
|
}
|
|
|
|
private static void AddFiller(TvContext context, int id, string name, FillerKind kind) =>
|
|
context.FillerPresets.Add(new FillerPreset
|
|
{
|
|
Id = id,
|
|
Name = name,
|
|
FillerKind = kind,
|
|
FillerMode = FillerMode.Count,
|
|
Count = 1,
|
|
CollectionType = CollectionType.Collection
|
|
});
|
|
}
|