Browse Source

add deco default filler (#1797)

* first pass at default filler for block scheduling

* configure default filler in ui

* update changelog
pull/1798/head
Jason Dove 1 year ago committed by GitHub
parent
commit
46f7289db8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 9
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  3. 6
      ErsatzTV.Application/Scheduling/Commands/UpdateDeco.cs
  4. 16
      ErsatzTV.Application/Scheduling/Commands/UpdateDecoHandler.cs
  5. 6
      ErsatzTV.Application/Scheduling/DecoViewModel.cs
  6. 6
      ErsatzTV.Application/Scheduling/Mapper.cs
  7. 12
      ErsatzTV.Core/Domain/Scheduling/Deco.cs
  8. 4
      ErsatzTV.Core/Domain/Scheduling/PlayoutHistory.cs
  9. 9
      ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutFillerBuilder.cs
  10. 30
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutEnumerator.cs
  11. 170
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs
  12. 15
      ErsatzTV.Core/Scheduling/BlockScheduling/HistoryDetails.cs
  13. 70
      ErsatzTV.Core/Scheduling/CollectionKey.cs
  14. 5855
      ErsatzTV.Infrastructure.MySql/Migrations/20240719173438_Add_Deco_DefaultFiller.Designer.cs
  15. 160
      ErsatzTV.Infrastructure.MySql/Migrations/20240719173438_Add_Deco_DefaultFiller.cs
  16. 5854
      ErsatzTV.Infrastructure.MySql/Migrations/20240719182313_Update_PlayoutHistory_BlockId_Optional.Designer.cs
  17. 36
      ErsatzTV.Infrastructure.MySql/Migrations/20240719182313_Update_PlayoutHistory_BlockId_Optional.cs
  18. 55
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  19. 5694
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240719161128_Add_Deco_DefaultFiller.Designer.cs
  20. 160
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240719161128_Add_Deco_DefaultFiller.cs
  21. 5693
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240719181902_Update_PlayoutHistory_BlockId_Optional.Designer.cs
  22. 36
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240719181902_Update_PlayoutHistory_BlockId_Optional.cs
  23. 55
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  24. 1
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/BlockConfiguration.cs
  25. 189
      ErsatzTV/Pages/DecoEditor.razor
  26. 1
      ErsatzTV/Startup.cs
  27. 8
      ErsatzTV/ViewModels/DecoEditViewModel.cs

4
CHANGELOG.md

@ -16,6 +16,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -16,6 +16,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- The `On Demand` setting will only be used for `Flood` playouts (NOT `Block` or `External JSON`)
- It is NOT recommended to use fixed start times with `On Demand` progress
- This will probably be disabled with a future update
- Add `Default Filler` to `Deco` system
- After all blocks are scheduled/added to the playout, a second pass will be made to insert filler
- Default filler will be shuffled and inserted in all unscheduled time between blocks
- Default filler will stop scheduling when the next item would extend into primary content
### Fixed
- Add basic cache busting to XMLTV image URLs

9
ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs

@ -18,6 +18,7 @@ namespace ErsatzTV.Application.Playouts; @@ -18,6 +18,7 @@ namespace ErsatzTV.Application.Playouts;
public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseError, Unit>>
{
private readonly IBlockPlayoutBuilder _blockPlayoutBuilder;
private readonly IBlockPlayoutFillerBuilder _blockPlayoutFillerBuilder;
private readonly IClient _client;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEntityLocker _entityLocker;
@ -31,6 +32,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -31,6 +32,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
IDbContextFactory<TvContext> dbContextFactory,
IPlayoutBuilder playoutBuilder,
IBlockPlayoutBuilder blockPlayoutBuilder,
IBlockPlayoutFillerBuilder blockPlayoutFillerBuilder,
IExternalJsonPlayoutBuilder externalJsonPlayoutBuilder,
IFFmpegSegmenterService ffmpegSegmenterService,
IEntityLocker entityLocker,
@ -40,6 +42,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -40,6 +42,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
_dbContextFactory = dbContextFactory;
_playoutBuilder = playoutBuilder;
_blockPlayoutBuilder = blockPlayoutBuilder;
_blockPlayoutFillerBuilder = blockPlayoutFillerBuilder;
_externalJsonPlayoutBuilder = externalJsonPlayoutBuilder;
_ffmpegSegmenterService = ffmpegSegmenterService;
_entityLocker = entityLocker;
@ -69,6 +72,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -69,6 +72,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
{
case ProgramSchedulePlayoutType.Block:
await _blockPlayoutBuilder.Build(playout, request.Mode, cancellationToken);
await _blockPlayoutFillerBuilder.Build(playout, request.Mode, cancellationToken);
break;
case ProgramSchedulePlayoutType.ExternalJson:
await _externalJsonPlayoutBuilder.Build(playout, request.Mode, cancellationToken);
@ -154,6 +158,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -154,6 +158,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
BuildPlayout buildPlayout) =>
dbContext.Playouts
.Include(p => p.Channel)
.Include(p => p.Deco)
.Include(p => p.Items)
.Include(p => p.PlayoutHistory)
.Include(p => p.Templates)
@ -161,6 +166,10 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -161,6 +166,10 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.ThenInclude(t => t.Items)
.ThenInclude(i => i.Block)
.ThenInclude(b => b.Items)
.Include(p => p.Templates)
.ThenInclude(t => t.DecoTemplate)
.ThenInclude(t => t.Items)
.ThenInclude(i => i.Deco)
.Include(p => p.FillGroupIndices)
.ThenInclude(fgi => fgi.EnumeratorState)
.Include(p => p.ProgramScheduleAlternates)

6
ErsatzTV.Application/Scheduling/Commands/UpdateDeco.cs

@ -10,6 +10,12 @@ public record UpdateDeco( @@ -10,6 +10,12 @@ public record UpdateDeco(
string Name,
DecoMode WatermarkMode,
int? WatermarkId,
DecoMode DefaultFillerMode,
ProgramScheduleItemCollectionType DefaultFillerCollectionType,
int? DefaultFillerCollectionId,
int? DefaultFillerMediaItemId,
int? DefaultFillerMultiCollectionId,
int? DefaultFillerSmartCollectionId,
DecoMode DeadAirFallbackMode,
ProgramScheduleItemCollectionType DeadAirFallbackCollectionType,
int? DeadAirFallbackCollectionId,

16
ErsatzTV.Application/Scheduling/Commands/UpdateDecoHandler.cs

@ -27,6 +27,22 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -27,6 +27,22 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
existing.WatermarkMode = request.WatermarkMode;
existing.WatermarkId = request.WatermarkMode is DecoMode.Override ? request.WatermarkId : null;
// default filler
existing.DefaultFillerMode = request.DefaultFillerMode;
existing.DefaultFillerCollectionType = request.DefaultFillerCollectionType;
existing.DefaultFillerCollectionId = request.DefaultFillerMode is DecoMode.Override
? request.DefaultFillerCollectionId
: null;
existing.DefaultFillerMediaItemId = request.DefaultFillerMode is DecoMode.Override
? request.DefaultFillerMediaItemId
: null;
existing.DefaultFillerMultiCollectionId = request.DefaultFillerMode is DecoMode.Override
? request.DefaultFillerMultiCollectionId
: null;
existing.DefaultFillerSmartCollectionId = request.DefaultFillerMode is DecoMode.Override
? request.DefaultFillerSmartCollectionId
: null;
// dead air fallback
existing.DeadAirFallbackMode = request.DeadAirFallbackMode;
existing.DeadAirFallbackCollectionType = request.DeadAirFallbackCollectionType;

6
ErsatzTV.Application/Scheduling/DecoViewModel.cs

@ -9,6 +9,12 @@ public record DecoViewModel( @@ -9,6 +9,12 @@ public record DecoViewModel(
string Name,
DecoMode WatermarkMode,
int? WatermarkId,
DecoMode DefaultFillerMode,
ProgramScheduleItemCollectionType DefaultFillerCollectionType,
int? DefaultFillerCollectionId,
int? DefaultFillerMediaItemId,
int? DefaultFillerMultiCollectionId,
int? DefaultFillerSmartCollectionId,
DecoMode DeadAirFallbackMode,
ProgramScheduleItemCollectionType DeadAirFallbackCollectionType,
int? DeadAirFallbackCollectionId,

6
ErsatzTV.Application/Scheduling/Mapper.cs

@ -56,6 +56,12 @@ internal static class Mapper @@ -56,6 +56,12 @@ internal static class Mapper
deco.Name,
deco.WatermarkMode,
deco.WatermarkId,
deco.DefaultFillerMode,
deco.DefaultFillerCollectionType,
deco.DefaultFillerCollectionId,
deco.DefaultFillerMediaItemId,
deco.DefaultFillerMultiCollectionId,
deco.DefaultFillerSmartCollectionId,
deco.DeadAirFallbackMode,
deco.DeadAirFallbackCollectionType,
deco.DeadAirFallbackCollectionId,

12
ErsatzTV.Core/Domain/Scheduling/Deco.cs

@ -13,6 +13,18 @@ public class Deco @@ -13,6 +13,18 @@ public class Deco
public int? WatermarkId { get; set; }
public ChannelWatermark Watermark { get; set; }
// default filler
public DecoMode DefaultFillerMode { get; set; }
public ProgramScheduleItemCollectionType DefaultFillerCollectionType { get; set; }
public int? DefaultFillerCollectionId { get; set; }
public Collection DefaultFillerCollection { get; set; }
public int? DefaultFillerMediaItemId { get; set; }
public MediaItem DefaultFillerMediaItem { get; set; }
public int? DefaultFillerMultiCollectionId { get; set; }
public MultiCollection DefaultFillerMultiCollection { get; set; }
public int? DefaultFillerSmartCollectionId { get; set; }
public SmartCollection DefaultFillerSmartCollection { get; set; }
// dead air fallback
public DecoMode DeadAirFallbackMode { get; set; }
public ProgramScheduleItemCollectionType DeadAirFallbackCollectionType { get; set; }

4
ErsatzTV.Core/Domain/Scheduling/PlayoutHistory.cs

@ -7,12 +7,12 @@ public class PlayoutHistory @@ -7,12 +7,12 @@ public class PlayoutHistory
public int PlayoutId { get; set; }
public Playout Playout { get; set; }
public int BlockId { get; set; }
public int? BlockId { get; set; }
public Block Block { get; set; }
public PlaybackOrder PlaybackOrder { get; set; }
public int Index { get; set; }
// something that uniquely identifies the collection within the block
// something that uniquely identifies the collection within the block
public string Key { get; set; }
// last occurence of an item from this collection in the playout

9
ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutFillerBuilder.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IBlockPlayoutFillerBuilder
{
Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken);
}

30
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutEnumerator.cs

@ -107,4 +107,34 @@ public static class BlockPlayoutEnumerator @@ -107,4 +107,34 @@ public static class BlockPlayoutEnumerator
// as long as already-played items are not included
return new BlockPlayoutShuffledMediaCollectionEnumerator(mediaItems, state);
}
public static IMediaCollectionEnumerator Shuffle(
List<MediaItem> collectionItems,
DateTimeOffset currentTime,
Playout playout,
Deco deco,
string historyKey)
{
DateTime historyTime = currentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory
.Filter(h => h.Key == historyKey)
.Filter(h => h.When < historyTime)
.OrderByDescending(h => h.When)
.HeadOrNone();
var state = new CollectionEnumeratorState { Seed = playout.Seed + deco.Id, Index = 0 };
foreach (PlayoutHistory h in maybeHistory)
{
state.Index = h.Index + 1;
}
// TODO: fix multi-collection groups, keep multi-part episodes together
var mediaItems = collectionItems
.Map(mi => new GroupedMediaItem(mi, null))
.ToList();
// it shouldn't matter which order the remaining items are shuffled in,
// as long as already-played items are not included
return new BlockPlayoutShuffledMediaCollectionEnumerator(mediaItems, state);
}
}

170
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs

@ -0,0 +1,170 @@ @@ -0,0 +1,170 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using Newtonsoft.Json;
namespace ErsatzTV.Core.Scheduling.BlockScheduling;
public class BlockPlayoutFillerBuilder(
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository,
IArtistRepository artistRepository) : IBlockPlayoutFillerBuilder
{
private static readonly JsonSerializerSettings JsonSettings = new()
{
NullValueHandling = NullValueHandling.Ignore
};
public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken)
{
if (mode is PlayoutBuildMode.Reset)
{
// remove all playout items with type filler
var toRemove = playout.Items.Where(pi => pi.FillerKind is not FillerKind.None).ToList();
foreach (PlayoutItem playoutItem in toRemove)
{
BlockPlayoutChangeDetection.RemoveItemAndHistory(playout, playoutItem);
}
}
var collectionEnumerators = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
// find all unscheduled periods
var queue = new Queue<PlayoutItem>(playout.Items);
while (queue.Count > 1)
{
PlayoutItem one = queue.Dequeue();
PlayoutItem two = queue.Peek();
DateTimeOffset start = one.FinishOffset;
DateTimeOffset finish = two.StartOffset;
// find applicable deco
foreach (Deco deco in GetDecoFor(playout, start))
{
var collectionKey = CollectionKey.ForDecoDefaultFiller(deco);
string historyKey = HistoryDetails.ForDefaultFiller(deco);
// load collection items from db on demand
if (!collectionEnumerators.TryGetValue(collectionKey, out IMediaCollectionEnumerator enumerator))
{
List<MediaItem> collectionItems = await MediaItemsForCollection.Collect(
mediaCollectionRepository,
televisionRepository,
artistRepository,
collectionKey);
enumerator = BlockPlayoutEnumerator.Shuffle(
collectionItems,
start,
playout,
deco,
historyKey);
collectionEnumerators.Add(collectionKey, enumerator);
}
DateTimeOffset current = start;
var pastTime = false;
while (current < finish)
{
foreach (MediaItem mediaItem in enumerator.Current)
{
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
// add filler from deco to unscheduled period
var filler = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = current.UtcDateTime,
Finish = current.UtcDateTime + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
FillerKind = FillerKind.Fallback,
CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings),
GuideStart = one.GuideStart,
GuideFinish = one.GuideFinish,
GuideGroup = one.GuideGroup
};
if (filler.FinishOffset > finish)
{
pastTime = true;
break;
}
playout.Items.Add(filler);
// create a playout history record
var nextHistory = new PlayoutHistory
{
PlayoutId = playout.Id,
PlaybackOrder = PlaybackOrder.Shuffle,
Index = enumerator.State.Index,
When = current.UtcDateTime,
Key = historyKey,
Details = HistoryDetails.ForMediaItem(mediaItem)
};
playout.PlayoutHistory.Add(nextHistory);
current += itemDuration;
enumerator.MoveNext();
}
if (pastTime)
{
break;
}
}
}
}
return playout;
}
private static Option<Deco> GetDecoFor(Playout playout, DateTimeOffset start)
{
Option<PlayoutTemplate> maybeTemplate = PlayoutTemplateSelector.GetPlayoutTemplateFor(playout.Templates, start);
foreach (PlayoutTemplate template in maybeTemplate)
{
if (template.DecoTemplate is not null)
{
foreach (DecoTemplateItem decoTemplateItem in template.DecoTemplate.Items)
{
if (decoTemplateItem.StartTime <= start.TimeOfDay && decoTemplateItem.EndTime > start.TimeOfDay)
{
switch (decoTemplateItem.Deco.DefaultFillerMode)
{
case DecoMode.Inherit:
return Optional(playout.Deco);
case DecoMode.Override:
return decoTemplateItem.Deco;
case DecoMode.Disable:
default:
return Option<Deco>.None;
}
}
}
}
}
return Optional(playout.Deco);
}
private static TimeSpan DurationForMediaItem(MediaItem mediaItem)
{
if (mediaItem is Image image)
{
return TimeSpan.FromSeconds(image.ImageMetadata.Head().DurationSeconds ?? Image.DefaultSeconds);
}
MediaVersion version = mediaItem.GetHeadVersion();
return version.Duration;
}
}

15
ErsatzTV.Core/Scheduling/BlockScheduling/HistoryDetails.cs

@ -28,6 +28,21 @@ internal static class HistoryDetails @@ -28,6 +28,21 @@ internal static class HistoryDetails
return JsonConvert.SerializeObject(key, Formatting.None, JsonSettings);
}
public static string ForDefaultFiller(Deco deco)
{
dynamic key = new
{
DecoId = deco.Id,
PlaybackOrder = PlaybackOrder.Shuffle,
CollectionType = deco.DefaultFillerCollectionType,
CollectionId = deco.DefaultFillerCollectionId,
MultiCollectionId = deco.DefaultFillerMultiCollectionId,
SmartCollectionId = deco.DefaultFillerSmartCollectionId,
};
return JsonConvert.SerializeObject(key, Formatting.None, JsonSettings);
}
public static string ForMediaItem(MediaItem mediaItem)
{
Details details = mediaItem switch

70
ErsatzTV.Core/Scheduling/CollectionKey.cs

@ -154,6 +154,76 @@ public class CollectionKey : Record<CollectionKey> @@ -154,6 +154,76 @@ public class CollectionKey : Record<CollectionKey>
_ => throw new ArgumentOutOfRangeException(nameof(item))
};
public static CollectionKey ForDecoDefaultFiller(Deco deco) =>
deco.DefaultFillerCollectionType switch
{
ProgramScheduleItemCollectionType.Collection => new CollectionKey
{
CollectionType = deco.DefaultFillerCollectionType,
CollectionId = deco.DefaultFillerCollectionId
},
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey
{
CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId
},
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey
{
CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId
},
ProgramScheduleItemCollectionType.Artist => new CollectionKey
{
CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId
},
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey
{
CollectionType = deco.DefaultFillerCollectionType,
MultiCollectionId = deco.DefaultFillerMultiCollectionId
},
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey
{
CollectionType = deco.DefaultFillerCollectionType,
SmartCollectionId = deco.DefaultFillerSmartCollectionId
},
ProgramScheduleItemCollectionType.FakeCollection => new CollectionKey
{
CollectionType = deco.DefaultFillerCollectionType
},
ProgramScheduleItemCollectionType.Movie => new CollectionKey
{
CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId
},
ProgramScheduleItemCollectionType.Episode => new CollectionKey
{
CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId
},
ProgramScheduleItemCollectionType.MusicVideo => new CollectionKey
{
CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId
},
ProgramScheduleItemCollectionType.OtherVideo => new CollectionKey
{
CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId
},
ProgramScheduleItemCollectionType.Song => new CollectionKey
{
CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId
},
ProgramScheduleItemCollectionType.Image => new CollectionKey
{
CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId
},
_ => throw new ArgumentOutOfRangeException(nameof(deco))
};
public static CollectionKey ForScheduleItem(ProgramScheduleItem item) =>
item.CollectionType switch
{

5855
ErsatzTV.Infrastructure.MySql/Migrations/20240719173438_Add_Deco_DefaultFiller.Designer.cs generated

File diff suppressed because it is too large Load Diff

160
ErsatzTV.Infrastructure.MySql/Migrations/20240719173438_Add_Deco_DefaultFiller.cs

@ -0,0 +1,160 @@ @@ -0,0 +1,160 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_Deco_DefaultFiller : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "DefaultFillerCollectionId",
table: "Deco",
type: "int",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "DefaultFillerCollectionType",
table: "Deco",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "DefaultFillerMediaItemId",
table: "Deco",
type: "int",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "DefaultFillerMode",
table: "Deco",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "DefaultFillerMultiCollectionId",
table: "Deco",
type: "int",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "DefaultFillerSmartCollectionId",
table: "Deco",
type: "int",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Deco_DefaultFillerCollectionId",
table: "Deco",
column: "DefaultFillerCollectionId");
migrationBuilder.CreateIndex(
name: "IX_Deco_DefaultFillerMediaItemId",
table: "Deco",
column: "DefaultFillerMediaItemId");
migrationBuilder.CreateIndex(
name: "IX_Deco_DefaultFillerMultiCollectionId",
table: "Deco",
column: "DefaultFillerMultiCollectionId");
migrationBuilder.CreateIndex(
name: "IX_Deco_DefaultFillerSmartCollectionId",
table: "Deco",
column: "DefaultFillerSmartCollectionId");
migrationBuilder.AddForeignKey(
name: "FK_Deco_Collection_DefaultFillerCollectionId",
table: "Deco",
column: "DefaultFillerCollectionId",
principalTable: "Collection",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Deco_MediaItem_DefaultFillerMediaItemId",
table: "Deco",
column: "DefaultFillerMediaItemId",
principalTable: "MediaItem",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Deco_MultiCollection_DefaultFillerMultiCollectionId",
table: "Deco",
column: "DefaultFillerMultiCollectionId",
principalTable: "MultiCollection",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Deco_SmartCollection_DefaultFillerSmartCollectionId",
table: "Deco",
column: "DefaultFillerSmartCollectionId",
principalTable: "SmartCollection",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Deco_Collection_DefaultFillerCollectionId",
table: "Deco");
migrationBuilder.DropForeignKey(
name: "FK_Deco_MediaItem_DefaultFillerMediaItemId",
table: "Deco");
migrationBuilder.DropForeignKey(
name: "FK_Deco_MultiCollection_DefaultFillerMultiCollectionId",
table: "Deco");
migrationBuilder.DropForeignKey(
name: "FK_Deco_SmartCollection_DefaultFillerSmartCollectionId",
table: "Deco");
migrationBuilder.DropIndex(
name: "IX_Deco_DefaultFillerCollectionId",
table: "Deco");
migrationBuilder.DropIndex(
name: "IX_Deco_DefaultFillerMediaItemId",
table: "Deco");
migrationBuilder.DropIndex(
name: "IX_Deco_DefaultFillerMultiCollectionId",
table: "Deco");
migrationBuilder.DropIndex(
name: "IX_Deco_DefaultFillerSmartCollectionId",
table: "Deco");
migrationBuilder.DropColumn(
name: "DefaultFillerCollectionId",
table: "Deco");
migrationBuilder.DropColumn(
name: "DefaultFillerCollectionType",
table: "Deco");
migrationBuilder.DropColumn(
name: "DefaultFillerMediaItemId",
table: "Deco");
migrationBuilder.DropColumn(
name: "DefaultFillerMode",
table: "Deco");
migrationBuilder.DropColumn(
name: "DefaultFillerMultiCollectionId",
table: "Deco");
migrationBuilder.DropColumn(
name: "DefaultFillerSmartCollectionId",
table: "Deco");
}
}
}

5854
ErsatzTV.Infrastructure.MySql/Migrations/20240719182313_Update_PlayoutHistory_BlockId_Optional.Designer.cs generated

File diff suppressed because it is too large Load Diff

36
ErsatzTV.Infrastructure.MySql/Migrations/20240719182313_Update_PlayoutHistory_BlockId_Optional.cs

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Update_PlayoutHistory_BlockId_Optional : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "BlockId",
table: "PlayoutHistory",
type: "int",
nullable: true,
oldClrType: typeof(int),
oldType: "int");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "BlockId",
table: "PlayoutHistory",
type: "int",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "int",
oldNullable: true);
}
}
}

55
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -2285,6 +2285,24 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2285,6 +2285,24 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int>("DecoGroupId")
.HasColumnType("int");
b.Property<int?>("DefaultFillerCollectionId")
.HasColumnType("int");
b.Property<int>("DefaultFillerCollectionType")
.HasColumnType("int");
b.Property<int?>("DefaultFillerMediaItemId")
.HasColumnType("int");
b.Property<int>("DefaultFillerMode")
.HasColumnType("int");
b.Property<int?>("DefaultFillerMultiCollectionId")
.HasColumnType("int");
b.Property<int?>("DefaultFillerSmartCollectionId")
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
@ -2304,6 +2322,14 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2304,6 +2322,14 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.HasIndex("DeadAirFallbackSmartCollectionId");
b.HasIndex("DefaultFillerCollectionId");
b.HasIndex("DefaultFillerMediaItemId");
b.HasIndex("DefaultFillerMultiCollectionId");
b.HasIndex("DefaultFillerSmartCollectionId");
b.HasIndex("WatermarkId");
b.HasIndex("DecoGroupId", "Name")
@ -2414,7 +2440,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2414,7 +2440,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BlockId")
b.Property<int?>("BlockId")
.HasColumnType("int");
b.Property<string>("Details")
@ -4660,6 +4686,22 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4660,6 +4686,22 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Collection", "DefaultFillerCollection")
.WithMany()
.HasForeignKey("DefaultFillerCollectionId");
b.HasOne("ErsatzTV.Core.Domain.MediaItem", "DefaultFillerMediaItem")
.WithMany()
.HasForeignKey("DefaultFillerMediaItemId");
b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "DefaultFillerMultiCollection")
.WithMany()
.HasForeignKey("DefaultFillerMultiCollectionId");
b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "DefaultFillerSmartCollection")
.WithMany()
.HasForeignKey("DefaultFillerSmartCollectionId");
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
.WithMany()
.HasForeignKey("WatermarkId")
@ -4675,6 +4717,14 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4675,6 +4717,14 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("DecoGroup");
b.Navigation("DefaultFillerCollection");
b.Navigation("DefaultFillerMediaItem");
b.Navigation("DefaultFillerMultiCollection");
b.Navigation("DefaultFillerSmartCollection");
b.Navigation("Watermark");
});
@ -4713,8 +4763,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4713,8 +4763,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block")
.WithMany("PlayoutHistory")
.HasForeignKey("BlockId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("PlayoutHistory")

5694
ErsatzTV.Infrastructure.Sqlite/Migrations/20240719161128_Add_Deco_DefaultFiller.Designer.cs generated

File diff suppressed because it is too large Load Diff

160
ErsatzTV.Infrastructure.Sqlite/Migrations/20240719161128_Add_Deco_DefaultFiller.cs

@ -0,0 +1,160 @@ @@ -0,0 +1,160 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_Deco_DefaultFiller : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "DefaultFillerCollectionId",
table: "Deco",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "DefaultFillerCollectionType",
table: "Deco",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "DefaultFillerMediaItemId",
table: "Deco",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "DefaultFillerMode",
table: "Deco",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "DefaultFillerMultiCollectionId",
table: "Deco",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "DefaultFillerSmartCollectionId",
table: "Deco",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Deco_DefaultFillerCollectionId",
table: "Deco",
column: "DefaultFillerCollectionId");
migrationBuilder.CreateIndex(
name: "IX_Deco_DefaultFillerMediaItemId",
table: "Deco",
column: "DefaultFillerMediaItemId");
migrationBuilder.CreateIndex(
name: "IX_Deco_DefaultFillerMultiCollectionId",
table: "Deco",
column: "DefaultFillerMultiCollectionId");
migrationBuilder.CreateIndex(
name: "IX_Deco_DefaultFillerSmartCollectionId",
table: "Deco",
column: "DefaultFillerSmartCollectionId");
migrationBuilder.AddForeignKey(
name: "FK_Deco_Collection_DefaultFillerCollectionId",
table: "Deco",
column: "DefaultFillerCollectionId",
principalTable: "Collection",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Deco_MediaItem_DefaultFillerMediaItemId",
table: "Deco",
column: "DefaultFillerMediaItemId",
principalTable: "MediaItem",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Deco_MultiCollection_DefaultFillerMultiCollectionId",
table: "Deco",
column: "DefaultFillerMultiCollectionId",
principalTable: "MultiCollection",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Deco_SmartCollection_DefaultFillerSmartCollectionId",
table: "Deco",
column: "DefaultFillerSmartCollectionId",
principalTable: "SmartCollection",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Deco_Collection_DefaultFillerCollectionId",
table: "Deco");
migrationBuilder.DropForeignKey(
name: "FK_Deco_MediaItem_DefaultFillerMediaItemId",
table: "Deco");
migrationBuilder.DropForeignKey(
name: "FK_Deco_MultiCollection_DefaultFillerMultiCollectionId",
table: "Deco");
migrationBuilder.DropForeignKey(
name: "FK_Deco_SmartCollection_DefaultFillerSmartCollectionId",
table: "Deco");
migrationBuilder.DropIndex(
name: "IX_Deco_DefaultFillerCollectionId",
table: "Deco");
migrationBuilder.DropIndex(
name: "IX_Deco_DefaultFillerMediaItemId",
table: "Deco");
migrationBuilder.DropIndex(
name: "IX_Deco_DefaultFillerMultiCollectionId",
table: "Deco");
migrationBuilder.DropIndex(
name: "IX_Deco_DefaultFillerSmartCollectionId",
table: "Deco");
migrationBuilder.DropColumn(
name: "DefaultFillerCollectionId",
table: "Deco");
migrationBuilder.DropColumn(
name: "DefaultFillerCollectionType",
table: "Deco");
migrationBuilder.DropColumn(
name: "DefaultFillerMediaItemId",
table: "Deco");
migrationBuilder.DropColumn(
name: "DefaultFillerMode",
table: "Deco");
migrationBuilder.DropColumn(
name: "DefaultFillerMultiCollectionId",
table: "Deco");
migrationBuilder.DropColumn(
name: "DefaultFillerSmartCollectionId",
table: "Deco");
}
}
}

5693
ErsatzTV.Infrastructure.Sqlite/Migrations/20240719181902_Update_PlayoutHistory_BlockId_Optional.Designer.cs generated

File diff suppressed because it is too large Load Diff

36
ErsatzTV.Infrastructure.Sqlite/Migrations/20240719181902_Update_PlayoutHistory_BlockId_Optional.cs

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Update_PlayoutHistory_BlockId_Optional : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "BlockId",
table: "PlayoutHistory",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "BlockId",
table: "PlayoutHistory",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
}
}
}

55
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -2170,6 +2170,24 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2170,6 +2170,24 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("DecoGroupId")
.HasColumnType("INTEGER");
b.Property<int?>("DefaultFillerCollectionId")
.HasColumnType("INTEGER");
b.Property<int>("DefaultFillerCollectionType")
.HasColumnType("INTEGER");
b.Property<int?>("DefaultFillerMediaItemId")
.HasColumnType("INTEGER");
b.Property<int>("DefaultFillerMode")
.HasColumnType("INTEGER");
b.Property<int?>("DefaultFillerMultiCollectionId")
.HasColumnType("INTEGER");
b.Property<int?>("DefaultFillerSmartCollectionId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
@ -2189,6 +2207,14 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2189,6 +2207,14 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("DeadAirFallbackSmartCollectionId");
b.HasIndex("DefaultFillerCollectionId");
b.HasIndex("DefaultFillerMediaItemId");
b.HasIndex("DefaultFillerMultiCollectionId");
b.HasIndex("DefaultFillerSmartCollectionId");
b.HasIndex("WatermarkId");
b.HasIndex("DecoGroupId", "Name")
@ -2289,7 +2315,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2289,7 +2315,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("BlockId")
b.Property<int?>("BlockId")
.HasColumnType("INTEGER");
b.Property<string>("Details")
@ -4499,6 +4525,22 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4499,6 +4525,22 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Collection", "DefaultFillerCollection")
.WithMany()
.HasForeignKey("DefaultFillerCollectionId");
b.HasOne("ErsatzTV.Core.Domain.MediaItem", "DefaultFillerMediaItem")
.WithMany()
.HasForeignKey("DefaultFillerMediaItemId");
b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "DefaultFillerMultiCollection")
.WithMany()
.HasForeignKey("DefaultFillerMultiCollectionId");
b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "DefaultFillerSmartCollection")
.WithMany()
.HasForeignKey("DefaultFillerSmartCollectionId");
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
.WithMany()
.HasForeignKey("WatermarkId")
@ -4514,6 +4556,14 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4514,6 +4556,14 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("DecoGroup");
b.Navigation("DefaultFillerCollection");
b.Navigation("DefaultFillerMediaItem");
b.Navigation("DefaultFillerMultiCollection");
b.Navigation("DefaultFillerSmartCollection");
b.Navigation("Watermark");
});
@ -4552,8 +4602,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4552,8 +4602,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block")
.WithMany("PlayoutHistory")
.HasForeignKey("BlockId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("PlayoutHistory")

1
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/BlockConfiguration.cs

@ -26,6 +26,7 @@ public class BlockConfiguration : IEntityTypeConfiguration<Block> @@ -26,6 +26,7 @@ public class BlockConfiguration : IEntityTypeConfiguration<Block>
builder.HasMany(b => b.PlayoutHistory)
.WithOne(h => h.Block)
.HasForeignKey(h => h.BlockId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
}
}

189
ErsatzTV/Pages/DecoEditor.razor

@ -53,28 +53,27 @@ @@ -53,28 +53,27 @@
<MudTooltip Style="max-width: 350px">
<ChildContent>
<MudText Typo="Typo.h6" Class="d-flex align-center justify-center">
Dead Air Fallback
Default Filler
&nbsp;
<MudIcon Icon="@Icons.Material.Filled.Info"/>
</MudText>
</ChildContent>
<TooltipContent>
<MudText Typo="Typo.body2">When no playout item is found for the current time, *one* item will be randomly selected from this collection and looped and cut to exactly fit until the start of the next playout item.</MudText>
<MudText Typo="Typo.body2" Class="mt-3">This replaces the "Channel is Offline" image that would otherwise display.</MudText>
<MudText Typo="Typo.body2">After all blocks have been scheduled, a second pass will be made to fill unscheduled time using random items from this collection.</MudText>
</TooltipContent>
</MudTooltip>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudSelect Label="Dead Air Fallback Mode" @bind-Value="_deco.DeadAirFallbackMode" For="@(() => _deco.DeadAirFallbackMode)">
<MudSelect Label="Default Filler Mode" @bind-Value="_deco.DefaultFillerMode" For="@(() => _deco.DefaultFillerMode)">
<MudSelectItem Value="DecoMode.Inherit">Inherit</MudSelectItem>
<MudSelectItem Value="DecoMode.Disable">Disable</MudSelectItem>
<MudSelectItem Value="DecoMode.Override">Override</MudSelectItem>
</MudSelect>
<MudSelect Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Dead Air Fallback Collection Type"
@bind-Value="_deco.DeadAirFallbackCollectionType"
For="@(() => _deco.DeadAirFallbackCollectionType)">
<MudSelect Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Default Filler Collection Type"
@bind-Value="_deco.DefaultFillerCollectionType"
For="@(() => _deco.DefaultFillerCollectionType)">
<MudSelectItem Value="ProgramScheduleItemCollectionType.Collection">Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionShow">Television Show</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionSeason">Television Season</MudSelectItem>
@ -82,78 +81,78 @@ @@ -82,78 +81,78 @@
<MudSelectItem Value="ProgramScheduleItemCollectionType.MultiCollection">Multi Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.SmartCollection">Smart Collection</MudSelectItem>
</MudSelect>
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.Collection)
@if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.Collection)
{
<MudSelect Class="mt-3"
T="MediaCollectionViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Collection"
@bind-value="_deco.DeadAirFallbackCollection">
@bind-value="_deco.DefaultFillerCollection">
@foreach (MediaCollectionViewModel collection in _mediaCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.MultiCollection)
@if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.MultiCollection)
{
<MudSelect Class="mt-3"
T="MultiCollectionViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Multi Collection"
@bind-value="_deco.DeadAirFallbackMultiCollection">
@bind-value="_deco.DefaultFillerMultiCollection">
@foreach (MultiCollectionViewModel collection in _multiCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.SmartCollection)
@if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.SmartCollection)
{
<MudSelect Class="mt-3"
T="SmartCollectionViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Smart Collection"
@bind-value="_deco.DeadAirFallbackSmartCollection">
@bind-value="_deco.DefaultFillerSmartCollection">
@foreach (SmartCollectionViewModel collection in _smartCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
@if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Television Show"
@bind-value="_deco.DeadAirFallbackMediaItem">
@bind-value="_deco.DefaultFillerMediaItem">
@foreach (NamedMediaItemViewModel show in _televisionShows)
{
<MudSelectItem Value="@show">@show.Name</MudSelectItem>
}
</MudSelect>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
@if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Television Season"
@bind-value="_deco.DeadAirFallbackMediaItem">
@bind-value="_deco.DefaultFillerMediaItem">
@foreach (NamedMediaItemViewModel season in _televisionSeasons)
{
<MudSelectItem Value="@season">@season.Name</MudSelectItem>
}
</MudSelect>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.Artist)
@if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.Artist)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Artist"
@bind-value="_deco.DeadAirFallbackMediaItem">
@bind-value="_deco.DefaultFillerMediaItem">
@foreach (NamedMediaItemViewModel artist in _artists)
{
<MudSelectItem Value="@artist">@artist.Name</MudSelectItem>
@ -162,6 +161,121 @@ @@ -162,6 +161,121 @@
}
</MudCardContent>
</MudCard>
<MudCard Class="mb-6" Style="width: 350px">
<MudCardHeader>
<CardHeaderContent>
<MudTooltip Style="max-width: 350px">
<ChildContent>
<MudText Typo="Typo.h6" Class="d-flex align-center justify-center">
Dead Air Fallback
&nbsp;
<MudIcon Icon="@Icons.Material.Filled.Info"/>
</MudText>
</ChildContent>
<TooltipContent>
<MudText Typo="Typo.body2">When no playout item is found for the current time, *one* item will be randomly selected from this collection and looped and cut to exactly fit until the start of the next playout item.</MudText>
<MudText Typo="Typo.body2" Class="mt-3">This replaces the "Channel is Offline" image that would otherwise display.</MudText>
</TooltipContent>
</MudTooltip>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudSelect Label="Dead Air Fallback Mode" @bind-Value="_deco.DeadAirFallbackMode" For="@(() => _deco.DeadAirFallbackMode)">
<MudSelectItem Value="DecoMode.Inherit">Inherit</MudSelectItem>
<MudSelectItem Value="DecoMode.Disable">Disable</MudSelectItem>
<MudSelectItem Value="DecoMode.Override">Override</MudSelectItem>
</MudSelect>
<MudSelect Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Dead Air Fallback Collection Type"
@bind-Value="_deco.DeadAirFallbackCollectionType"
For="@(() => _deco.DeadAirFallbackCollectionType)">
<MudSelectItem Value="ProgramScheduleItemCollectionType.Collection">Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionShow">Television Show</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionSeason">Television Season</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.Artist">Artist</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.MultiCollection">Multi Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.SmartCollection">Smart Collection</MudSelectItem>
</MudSelect>
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.Collection)
{
<MudSelect Class="mt-3"
T="MediaCollectionViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Collection"
@bind-value="_deco.DeadAirFallbackCollection">
@foreach (MediaCollectionViewModel collection in _mediaCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.MultiCollection)
{
<MudSelect Class="mt-3"
T="MultiCollectionViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Multi Collection"
@bind-value="_deco.DeadAirFallbackMultiCollection">
@foreach (MultiCollectionViewModel collection in _multiCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.SmartCollection)
{
<MudSelect Class="mt-3"
T="SmartCollectionViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Smart Collection"
@bind-value="_deco.DeadAirFallbackSmartCollection">
@foreach (SmartCollectionViewModel collection in _smartCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Television Show"
@bind-value="_deco.DeadAirFallbackMediaItem">
@foreach (NamedMediaItemViewModel show in _televisionShows)
{
<MudSelectItem Value="@show">@show.Name</MudSelectItem>
}
</MudSelect>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Television Season"
@bind-value="_deco.DeadAirFallbackMediaItem">
@foreach (NamedMediaItemViewModel season in _televisionSeasons)
{
<MudSelectItem Value="@season">@season.Name</MudSelectItem>
}
</MudSelect>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.Artist)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Artist"
@bind-value="_deco.DeadAirFallbackMediaItem">
@foreach (NamedMediaItemViewModel artist in _artists)
{
<MudSelectItem Value="@artist">@artist.Name</MudSelectItem>
}
</MudSelect>
}
</MudCardContent>
</MudCard>
</MudStack>
</MudContainer>
@ -229,6 +343,23 @@ @@ -229,6 +343,23 @@
DecoGroupId = deco.DecoGroupId,
WatermarkMode = deco.WatermarkMode,
WatermarkId = deco.WatermarkId,
DefaultFillerMode = deco.DefaultFillerMode,
DefaultFillerCollectionType = deco.DefaultFillerCollectionType,
DefaultFillerCollection = deco.DefaultFillerCollectionId.HasValue
? _mediaCollections.Find(c => c.Id == deco.DefaultFillerCollectionId.Value)
: null,
DefaultFillerMediaItem = deco.DefaultFillerMediaItemId.HasValue
? _televisionShows.Append(_televisionSeasons).Append(_artists).ToList()
.Find(vm => vm.MediaItemId == deco.DefaultFillerMediaItemId.Value)
: null,
DefaultFillerMultiCollection = deco.DefaultFillerMultiCollectionId.HasValue
? _multiCollections.Find(c => c.Id == deco.DefaultFillerMultiCollectionId.Value)
: null,
DefaultFillerSmartCollection = deco.DefaultFillerSmartCollectionId.HasValue
? _smartCollections.Find(c => c.Id == deco.DefaultFillerSmartCollectionId.Value)
: null,
DeadAirFallbackMode = deco.DeadAirFallbackMode,
DeadAirFallbackCollectionType = deco.DeadAirFallbackCollectionType,
DeadAirFallbackCollection = deco.DeadAirFallbackCollectionId.HasValue
@ -256,6 +387,12 @@ @@ -256,6 +387,12 @@
_deco.Name,
_deco.WatermarkMode,
_deco.WatermarkId,
_deco.DefaultFillerMode,
_deco.DefaultFillerCollectionType,
_deco.DefaultFillerCollection?.Id,
_deco.DefaultFillerMediaItem?.MediaItemId,
_deco.DefaultFillerMultiCollection?.Id,
_deco.DefaultFillerSmartCollection?.Id,
_deco.DeadAirFallbackMode,
_deco.DeadAirFallbackCollectionType,
_deco.DeadAirFallbackCollection?.Id,

1
ErsatzTV/Startup.cs

@ -654,6 +654,7 @@ public class Startup @@ -654,6 +654,7 @@ public class Startup
services.AddScoped<IPlayoutBuilder, PlayoutBuilder>();
services.AddScoped<IBlockPlayoutBuilder, BlockPlayoutBuilder>();
services.AddScoped<IBlockPlayoutPreviewBuilder, BlockPlayoutPreviewBuilder>();
services.AddScoped<IBlockPlayoutFillerBuilder, BlockPlayoutFillerBuilder>();
services.AddScoped<IExternalJsonPlayoutBuilder, ExternalJsonPlayoutBuilder>();
services.AddScoped<IPlayoutTimeShifter, PlayoutTimeShifter>();
services.AddScoped<IImageCache, ImageCache>();

8
ErsatzTV/ViewModels/DecoEditViewModel.cs

@ -11,6 +11,14 @@ public class DecoEditViewModel @@ -11,6 +11,14 @@ public class DecoEditViewModel
public string Name { get; set; }
public DecoMode WatermarkMode { get; set; }
public int? WatermarkId { get; set; }
public DecoMode DefaultFillerMode { get; set; }
public ProgramScheduleItemCollectionType DefaultFillerCollectionType { get; set; }
public MediaCollectionViewModel DefaultFillerCollection { get; set; }
public MultiCollectionViewModel DefaultFillerMultiCollection { get; set; }
public SmartCollectionViewModel DefaultFillerSmartCollection { get; set; }
public NamedMediaItemViewModel DefaultFillerMediaItem { get; set; }
public DecoMode DeadAirFallbackMode { get; set; }
public ProgramScheduleItemCollectionType DeadAirFallbackCollectionType { get; set; }
public MediaCollectionViewModel DeadAirFallbackCollection { get; set; }

Loading…
Cancel
Save