filler rework (#449)

* add chapter statistics and new filler options

* refactor playout builder

* more refactor prep for filler

* rewrite schedulers

* refactor collectionkey

* add tail filler kind

* migrate tail filler to filler preset

* optionally show filler

* fix playout detail row count

* remove duration tail filler options

* implement tail and fallback in flood scheduler

* implement tail and fallback in one scheduler

* implement tail and fallback in multiple scheduler

* implement looping fallback filler

* more duration tests

* start to add post-roll filler to flood

* rework playoutitem filler tagging

* rework scheduler logging

* calculate whether configured filler will fit

* implement pre-roll and post-roll duration and count filler

* improve duration filler calculation

* add minutes to search index

* update channel guide to work with new filler

* add mid-roll filler

* don't clone enumerators for filler calculations

* support pre-roll and post-roll pad filler

* implement mid-roll pad filler

* allow clearing filler selections in schedule editor

* fix tests

* filler config validation

* use consistent time zone for tests
This commit is contained in:
Jason Dove
2021-10-21 20:23:14 -05:00
committed by GitHub
parent f4a63a1a1a
commit 6d147de2f3
133 changed files with 54673 additions and 990 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ namespace ErsatzTV.Controllers
{
Process process = processModel.Process;
_logger.LogDebug(
_logger.LogInformation(
"ffmpeg arguments {FFmpegArguments}",
string.Join(" ", process.StartInfo.ArgumentList));
process.Start();
+1 -1
View File
@@ -213,7 +213,7 @@
DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule)
{
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, PlaybackOrder.Shuffle, null, null, TailMode.None, ProgramScheduleItemCollectionType.Collection, null, null, null, null, null, GuideMode.Normal));
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null));
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
}
}
+2 -2
View File
@@ -36,7 +36,7 @@
private readonly CollectionEditViewModel _model = new();
private EditContext _editContext;
private ValidationMessageStore _messageStore;
protected override async Task OnParametersSetAsync()
{
if (IsEdit)
@@ -59,7 +59,7 @@
_editContext = new EditContext(_model);
_messageStore = new ValidationMessageStore(_editContext);
}
private bool IsEdit => Id != 0;
private async Task HandleSubmitAsync()
+227
View File
@@ -0,0 +1,227 @@
@page "/media/filler/presets/{Id:int}/edit"
@page "/media/filler/presets/add"
@using ErsatzTV.Application.Filler
@using ErsatzTV.Application.Filler.Queries
@using ErsatzTV.Application.Television.Queries
@using ErsatzTV.Application.Artists.Queries
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Queries
@using ErsatzTV.Application.MediaItems
@using ErsatzTV.Core.Domain.Filler
@using Microsoft.AspNetCore.Components
@inject NavigationManager _navigationManager
@inject ILogger<FillerPresetEditor> _logger
@inject ISnackbar _snackbar
@inject IMediator _mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<div style="max-width: 400px;">
<MudText Typo="Typo.h4" Class="mb-4">@(IsEdit ? "Edit Filler Preset" : "Add Filler Preset")</MudText>
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">
<FluentValidator/>
<MudCard>
<MudCardContent>
<MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/>
<MudSelect Class="mt-3" Label="Filler Kind" @bind-Value="_model.FillerKind" For="@(() => _model.FillerKind)" Disabled="@IsEdit">
<MudSelectItem Value="@(FillerKind.PreRoll)">Pre-Roll</MudSelectItem>
<MudSelectItem Value="@(FillerKind.MidRoll)">Mid-Roll</MudSelectItem>
<MudSelectItem Value="@(FillerKind.PostRoll)">Post-Roll</MudSelectItem>
<MudSelectItem Value="@(FillerKind.Tail)">Tail</MudSelectItem>
<MudSelectItem Value="@(FillerKind.Fallback)">Fallback</MudSelectItem>
</MudSelect>
<MudSelect Class="mt-3" Label="Filler Mode" @bind-Value="_model.FillerMode" For="@(() => _model.FillerMode)"
Disabled="@(_model.FillerKind is FillerKind.Fallback or FillerKind.Tail)">
<MudSelectItem Value="@(FillerMode.Duration)">Duration</MudSelectItem>
<MudSelectItem Value="@(FillerMode.Count)">Count</MudSelectItem>
<MudSelectItem Value="@(FillerMode.Pad)">Pad</MudSelectItem>
</MudSelect>
<MudTimePicker Class="mt-3" Label="Filler Duration" @bind-Time="@_model.Duration" For="@(() => _model.Duration)" Disabled="@(_model.FillerMode != FillerMode.Duration)"/>
<MudTextField Class="mt-3" Label="Filler Count" @bind-Value="@_model.Count" For="@(() => _model.Count)" Disabled="@(_model.FillerMode != FillerMode.Count)"/>
<MudSelect Class="mt-3" Label="Filler Pad To Nearest Minute" @bind-Value="_model.PadToNearestMinute" For="@(() => _model.PadToNearestMinute)" Disabled="@(_model.FillerMode != FillerMode.Pad)">
<MudSelectItem T="int?" Value="5">5 (:00, :05, :10, :15, :20, etc)</MudSelectItem>
<MudSelectItem T="int?" Value="10">10 (:00, :10, :20, :30, :40, :50)</MudSelectItem>
<MudSelectItem T="int?" Value="15">15 (:00, :15, :30, :45)</MudSelectItem>
<MudSelectItem T="int?" Value="30">30 (:00, :30)</MudSelectItem>
</MudSelect>
<MudSelect Class="mt-3" Label="Filler Collection Type" @bind-Value="_model.CollectionType" For="@(() => _model.CollectionType)">
@foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues<ProgramScheduleItemCollectionType>())
{
<MudSelectItem Value="@collectionType">@collectionType</MudSelectItem>
}
</MudSelect>
@if (_model.CollectionType == ProgramScheduleItemCollectionType.Collection)
{
<MudAutocomplete Class="mt-3"
T="MediaCollectionViewModel"
Label="Collection"
@bind-value="_model.Collection"
SearchFunc="@SearchMediaCollections"
ToStringFunc="@(c => c?.Name)"/>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.MultiCollection)
{
<MudAutocomplete Class="mt-3"
T="MultiCollectionViewModel"
Label="Multi Collection"
@bind-value="_model.MultiCollection"
SearchFunc="@SearchMultiCollections"
ToStringFunc="@(c => c?.Name)"/>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.SmartCollection)
{
<MudAutocomplete Class="mt-3"
T="SmartCollectionViewModel"
Label="Smart Collection"
@bind-value="_model.SmartCollection"
SearchFunc="@SearchSmartCollections"
ToStringFunc="@(c => c?.Name)"/>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
{
<MudAutocomplete Class="mt-3"
T="NamedMediaItemViewModel"
Label="Television Show"
@bind-value="_model.MediaItem"
SearchFunc="@SearchTelevisionShows"
ToStringFunc="@(s => s?.Name)"/>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
{
<MudAutocomplete Class="mt-3"
T="NamedMediaItemViewModel"
Label="Television Season"
@bind-value="_model.MediaItem"
SearchFunc="@SearchTelevisionSeasons"
ToStringFunc="@(s => s?.Name)"/>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.Artist)
{
<MudAutocomplete Class="mt-3"
T="NamedMediaItemViewModel"
Label="Artist"
@bind-value="_model.MediaItem"
SearchFunc="@SearchArtists"
ToStringFunc="@(s => s?.Name)"/>
}
</MudCardContent>
<MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">
@(IsEdit ? "Save Changes" : "Add Filler Preset")
</MudButton>
</MudCardActions>
</MudCard>
</EditForm>
</div>
</MudContainer>
@code {
[Parameter]
public int Id { get; set; }
private readonly FillerPresetEditViewModel _model = new();
private EditContext _editContext;
private ValidationMessageStore _messageStore;
private List<MediaCollectionViewModel> _mediaCollections;
private List<MultiCollectionViewModel> _multiCollections;
private List<SmartCollectionViewModel> _smartCollections;
private List<NamedMediaItemViewModel> _televisionShows;
private List<NamedMediaItemViewModel> _televisionSeasons;
private List<NamedMediaItemViewModel> _artists;
protected override async Task OnParametersSetAsync()
{
_mediaCollections = await _mediator.Send(new GetAllCollections())
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_multiCollections = await _mediator.Send(new GetAllMultiCollections())
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_smartCollections = await _mediator.Send(new GetAllSmartCollections())
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_televisionShows = await _mediator.Send(new GetAllTelevisionShows())
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_televisionSeasons = await _mediator.Send(new GetAllTelevisionSeasons())
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_artists = await _mediator.Send(new GetAllArtists())
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
if (IsEdit)
{
Option<FillerPresetViewModel> maybeFillerPreset = await _mediator.Send(new GetFillerPresetById(Id));
maybeFillerPreset.IfSome(fillerPreset =>
{
_model.Id = fillerPreset.Id;
_model.Name = fillerPreset.Name;
_model.FillerKind = fillerPreset.FillerKind;
_model.FillerMode = fillerPreset.FillerMode;
_model.Duration = fillerPreset.Duration;
_model.Count = fillerPreset.Count;
_model.PadToNearestMinute = fillerPreset.PadToNearestMinute;
_model.CollectionType = fillerPreset.CollectionType;
_model.Collection = fillerPreset.CollectionId.HasValue
? _mediaCollections.Find(c => c.Id == fillerPreset.CollectionId.Value)
: null;
_model.MultiCollection = fillerPreset.MultiCollectionId.HasValue
? _multiCollections.Find(c => c.Id == fillerPreset.MultiCollectionId.Value)
: null;
_model.SmartCollection = fillerPreset.SmartCollectionId.HasValue
? _smartCollections.Find(c => c.Id == fillerPreset.SmartCollectionId.Value)
: null;
_model.MediaItem = fillerPreset.MediaItemId.HasValue
? _televisionShows.Append(_televisionSeasons).Append(_artists).ToList().Find(vm => vm.MediaItemId == fillerPreset.MediaItemId.Value)
: null;
});
}
else
{
_model.Name = "New Filler Preset";
_model.FillerKind = FillerKind.PreRoll;
_model.FillerMode = FillerMode.Duration;
}
}
protected override void OnInitialized()
{
_editContext = new EditContext(_model);
_messageStore = new ValidationMessageStore(_editContext);
}
private bool IsEdit => Id != 0;
private async Task HandleSubmitAsync()
{
_messageStore.Clear();
if (_editContext.Validate())
{
IRequest<Either<BaseError, LanguageExt.Unit>> request = IsEdit ? _model.ToEdit() : _model.ToUpdate();
Seq<BaseError> errorMessage = await _mediator.Send(request)
.Map(result => result.LeftToSeq());
errorMessage.HeadOrNone().Match(
error =>
{
_snackbar.Add(error.Value, Severity.Error);
_logger.LogError("Error saving filler preset: {Error}", error.Value);
},
() => _navigationManager.NavigateTo("/media/filler/presets"));
}
}
private Task<IEnumerable<MediaCollectionViewModel>> SearchMediaCollections(string value) =>
_mediaCollections.Filter(c => c.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private Task<IEnumerable<MultiCollectionViewModel>> SearchMultiCollections(string value) =>
_multiCollections.Filter(c => c.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private Task<IEnumerable<SmartCollectionViewModel>> SearchSmartCollections(string value) =>
_smartCollections.Filter(c => c.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private Task<IEnumerable<NamedMediaItemViewModel>> SearchTelevisionShows(string value) =>
_televisionShows.Filter(s => s.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private Task<IEnumerable<NamedMediaItemViewModel>> SearchTelevisionSeasons(string value) =>
_televisionSeasons.Filter(s => s.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private Task<IEnumerable<NamedMediaItemViewModel>> SearchArtists(string value) =>
_artists.Filter(s => s.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
}
+103
View File
@@ -0,0 +1,103 @@
@page "/media/filler/presets"
@using ErsatzTV.Application.Filler
@using ErsatzTV.Application.Configuration.Queries
@using ErsatzTV.Application.Configuration.Commands
@using ErsatzTV.Application.Filler.Commands
@using ErsatzTV.Application.Filler.Queries
@using ErsatzTV.Core.Domain.Filler
@inject IDialogService _dialog
@inject IMediator _mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Link="/media/filler/presets/add">
Add Filler Preset
</MudButton>
</div>
<MudTable Class="mt-4"
Hover="true"
@bind-RowsPerPage="@_fillerPresetsRowsPerPage"
ServerData="@(new Func<TableState, Task<TableData<FillerPresetViewModel>>>(ServerReloadFillerPresets))"
Dense="true"
@ref="_fillerPresetsTable">
<ToolBarContent>
<MudText Typo="Typo.h6">Filler Presets</MudText>
</ToolBarContent>
<ColGroup>
<col/>
<col/>
<col style="width: 120px;"/>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>Filler Kind</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd DataLabel="Filler Kind">
@(
context.FillerKind switch {
FillerKind.PreRoll => "Pre-Roll",
FillerKind.MidRoll => "Mid-Roll",
FillerKind.PostRoll => "Post-Roll",
FillerKind.Fallback => "Fallback",
FillerKind.Tail => "Tail",
_ => "None"
}
)
</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Edit Filler Preset">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Link="@($"/media/filler/presets/{context.Id}/edit")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Filler Preset">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteFillerPreset(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
</MudContainer>
@code {
private MudTable<FillerPresetViewModel> _fillerPresetsTable;
private int _fillerPresetsRowsPerPage;
protected override async Task OnParametersSetAsync()
{
_fillerPresetsRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.FillerPresetsPageSize))
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
}
private async Task DeleteFillerPreset(FillerPresetViewModel fillerPreset)
{
var parameters = new DialogParameters { { "EntityType", "filler preset" }, { "EntityName", fillerPreset.Name } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = _dialog.Show<DeleteDialog>("Delete Filler Preset", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Cancelled)
{
await _mediator.Send(new DeleteFillerPreset(fillerPreset.Id));
await _fillerPresetsTable.ReloadServerData();
}
}
private async Task<TableData<FillerPresetViewModel>> ServerReloadFillerPresets(TableState state)
{
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.FillerPresetsPageSize, state.PageSize.ToString()));
PagedFillerPresetsViewModel data = await _mediator.Send(new GetPagedFillerPresets(state.Page, state.PageSize));
return new TableData<FillerPresetViewModel> { TotalItems = data.TotalCount, Items = data.Page };
}
}
+31 -4
View File
@@ -8,8 +8,12 @@
@inject IMediator _mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Link="/playouts/add">
Add Playout
</MudButton>
<MudTable Hover="true"
Dense="true"
Class="mt-4"
SelectedItemChanged="@(async (PlayoutNameViewModel x) => await PlayoutSelected(x))"
@bind-RowsPerPage="@_rowsPerPage"
ServerData="@(new Func<TableState, Task<TableData<PlayoutNameViewModel>>>(ServerReload))"
@@ -64,9 +68,6 @@
<MudTablePager/>
</PagerContent>
</MudTable>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Link="/playouts/add" Class="mt-4">
Add Playout
</MudButton>
@if (_selectedPlayoutId != null)
{
@@ -78,6 +79,7 @@
@ref="_detailTable">
<ToolBarContent>
<MudText Typo="Typo.h6">Playout Detail</MudText>
<MudSwitch T="bool" Class="ml-6" @bind-Checked="@ShowFiller" Color="Color.Secondary" Label="Show Filler"/>
</ToolBarContent>
<HeaderContent>
<MudTh>Start</MudTh>
@@ -102,6 +104,20 @@
private int _rowsPerPage;
private int _detailRowsPerPage;
private int? _selectedPlayoutId;
private bool _showFiller;
private bool ShowFiller
{
get => _showFiller;
set
{
if (_showFiller != value)
{
_showFiller = value;
_detailTable.ReloadServerData();
}
}
}
protected override async Task OnParametersSetAsync()
{
@@ -109,6 +125,8 @@
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_detailRowsPerPage = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize))
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
_showFiller = await _mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller))
.Map(maybeShow => maybeShow.Match(ce => bool.TryParse(ce.Value, out bool show) && show, () => false));
}
private async Task PlayoutSelected(PlayoutNameViewModel playout)
@@ -117,6 +135,14 @@
await _detailTable.ReloadServerData();
}
private async Task ShowFillerChanged()
{
if (_selectedPlayoutId != null)
{
await _detailTable.ReloadServerData();
}
}
private async Task DeletePlayout(PlayoutNameViewModel playout)
{
var parameters = new DialogParameters { { "EntityType", "playout" }, { "EntityName", $"{playout.ScheduleName} on {playout.ChannelNumber} - {playout.ChannelName}" } };
@@ -178,11 +204,12 @@
private async Task<TableData<PlayoutItemViewModel>> DetailServerReload(TableState state)
{
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize, state.PageSize.ToString()));
await _mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller, _showFiller.ToString()));
if (_selectedPlayoutId.HasValue)
{
PagedPlayoutItemsViewModel data =
await _mediator.Send(new GetFuturePlayoutItemsById(_selectedPlayoutId.Value, state.Page, state.PageSize));
await _mediator.Send(new GetFuturePlayoutItemsById(_selectedPlayoutId.Value, _showFiller, state.Page, state.PageSize));
return new TableData<PlayoutItemViewModel>
{
TotalItems = data.TotalCount,
+86 -108
View File
@@ -7,6 +7,9 @@
@using ErsatzTV.Application.ProgramSchedules.Queries
@using ErsatzTV.Application.Television.Queries
@using ErsatzTV.Application.Artists.Queries
@using ErsatzTV.Application.Filler
@using ErsatzTV.Application.Filler.Queries
@using ErsatzTV.Core.Domain.Filler
@inject NavigationManager _navigationManager
@inject ILogger<ScheduleItemsEditor> _logger
@inject ISnackbar _snackbar
@@ -36,7 +39,7 @@
<RowTemplate>
<MudTd DataLabel="Start Time">
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
@(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic")
@(context.StartType == StartType.Fixed ? context.StartTime == null ? string.Empty : DateTime.Today.Add(context.StartTime.Value).ToShortTimeString() : "Dynamic")
</MudText>
</MudTd>
<MudTd DataLabel="Collection">
@@ -81,10 +84,10 @@
@if (_selectedItem is not null)
{
<div style="display: flex; flex-direction: row;" class="mt-6">
<div style="flex-grow: 1; max-width: 400px;">
<EditForm Model="_selectedItem">
<FluentValidator/>
<EditForm Model="_selectedItem">
<FluentValidator/>
<div style="display: flex; flex-direction: row;" class="mt-6">
<div style="flex-grow: 1; max-width: 400px;" class="mr-6">
<MudCard>
<MudCardContent>
<MudSelect Label="Start Type" @bind-Value="_selectedItem.StartType" For="@(() => _selectedItem.StartType)">
@@ -188,72 +191,6 @@
<MudSelectItem Value="@TailMode.Offline">Offline</MudSelectItem>
<MudSelectItem Value="@TailMode.Filler">Filler</MudSelectItem>
</MudSelect>
<MudSelect Class="mt-3" Label="Tail Collection Type" @bind-Value="_selectedItem.TailCollectionType" For="@(() => _selectedItem.TailCollectionType)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)">
@foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues<ProgramScheduleItemCollectionType>())
{
<MudSelectItem Value="@collectionType">@collectionType</MudSelectItem>
}
</MudSelect>
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.Collection)
{
<MudAutocomplete Class="mt-3"
T="MediaCollectionViewModel"
Label="Collection"
@bind-value="_selectedItem.TailCollection"
SearchFunc="@SearchMediaCollections"
ToStringFunc="@(c => c?.Name)"
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
}
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.MultiCollection)
{
<MudAutocomplete Class="mt-3"
T="MultiCollectionViewModel"
Label="Multi Collection"
@bind-value="_selectedItem.TailMultiCollection"
SearchFunc="@SearchMultiCollections"
ToStringFunc="@(c => c?.Name)"
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
}
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.SmartCollection)
{
<MudAutocomplete Class="mt-3"
T="SmartCollectionViewModel"
Label="Smart Collection"
@bind-value="_selectedItem.TailSmartCollection"
SearchFunc="@SearchSmartCollections"
ToStringFunc="@(c => c?.Name)"
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
}
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
{
<MudAutocomplete Class="mt-3"
T="NamedMediaItemViewModel"
Label="Television Show"
@bind-value="_selectedItem.TailMediaItem"
SearchFunc="@SearchTelevisionShows"
ToStringFunc="@(s => s?.Name)"
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
}
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
{
<MudAutocomplete Class="mt-3"
T="NamedMediaItemViewModel"
Label="Television Season"
@bind-value="_selectedItem.TailMediaItem"
SearchFunc="@SearchTelevisionSeasons"
ToStringFunc="@(s => s?.Name)"
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
}
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.Artist)
{
<MudAutocomplete Class="mt-3"
T="NamedMediaItemViewModel"
Label="Artist"
@bind-value="_selectedItem.TailMediaItem"
SearchFunc="@SearchArtists"
ToStringFunc="@(s => s?.Name)"
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
}
<MudTextField Class="mt-3" Label="Custom Title" @bind-Value="@_selectedItem.CustomTitle" For="@(() => _selectedItem.CustomTitle)"/>
<MudSelect Class="mt-3" Label="Guide Mode" @bind-Value="@_selectedItem.GuideMode" For="@(() => _selectedItem.GuideMode)">
<MudSelectItem Value="@GuideMode.Normal">Normal</MudSelectItem>
@@ -261,28 +198,55 @@
</MudSelect>
</MudCardContent>
</MudCard>
</EditForm>
</div>
<div style="flex-grow: 1; max-width: 400px;">
<MudCard>
<MudCardContent>
<MudAutocomplete Class="mt-3"
T="FillerPresetViewModel"
Label="Pre-Roll Filler"
@bind-value="_selectedItem.PreRollFiller"
SearchFunc="@(arg => SearchFillerPresets(FillerKind.PreRoll, arg))"
ToStringFunc="@(c => c?.Name)"
ResetValueOnEmptyText="true"
Clearable="true"/>
<MudAutocomplete Class="mt-3"
T="FillerPresetViewModel"
Label="Mid-Roll Filler"
@bind-value="_selectedItem.MidRollFiller"
SearchFunc="@(arg => SearchFillerPresets(FillerKind.MidRoll, arg))"
ToStringFunc="@(c => c?.Name)"
ResetValueOnEmptyText="true"
Clearable="true"/>
<MudAutocomplete Class="mt-3"
T="FillerPresetViewModel"
Label="Post-Roll Filler"
@bind-value="_selectedItem.PostRollFiller"
SearchFunc="@(arg => SearchFillerPresets(FillerKind.PostRoll, arg))"
ToStringFunc="@(c => c?.Name)"
ResetValueOnEmptyText="true"
Clearable="true"/>
<MudAutocomplete Class="mt-3"
T="FillerPresetViewModel"
Label="Tail Filler"
@bind-value="_selectedItem.TailFiller"
SearchFunc="@(arg => SearchFillerPresets(FillerKind.Tail, arg))"
ToStringFunc="@(c => c?.Name)"
ResetValueOnEmptyText="true"
Clearable="true"/>
<MudAutocomplete Class="mt-3"
T="FillerPresetViewModel"
Label="Fallback Filler"
@bind-value="_selectedItem.FallbackFiller"
SearchFunc="@(arg => SearchFillerPresets(FillerKind.Fallback, arg))"
ToStringFunc="@(c => c?.Name)"
ResetValueOnEmptyText="true"
Clearable="true"/>
</MudCardContent>
</MudCard>
</div>
</div>
<div style="max-width: 600px;" class="ml-6">
<MudCard>
<MudCardContent>
<MudText Class="mt-3" Typo="Typo.h6">
Start Type
</MudText>
<MudText Class="mt-3">
A <b>fixed</b> start type requires a <b>start time</b>, while a <b>dynamic</b> start type means the schedule item will start immediately after the preceding schedule item.
</MudText>
<MudText Class="mt-3 mb-2" Typo="Typo.h6">Playout Mode</MudText>
<ul class="mud-typography-body1">
<li><b>One</b> - to play one media item from the collection before advancing to the next schedule item.</li>
<li><b>Multiple</b> - to play a specified <b>count</b> of media items from the collection before advancing to the next schedule item. A count of <b>0</b> is a special case that will automatically and always equal the number of items in the collection, so each item from the collection will be played once before advancing.</li>
<li><b>Duration</b> - to play the maximum number of complete media items that will fit in the specified <b>playout duration</b>, before either going offline for the remainder of the <b>playout duration</b> (an <b>offline tail</b>), or immediately advancing to the next schedule item.</li>
<li><b>Flood</b> - to play media items from the collection forever, or until the next schedule item's <b>start time</b> if one exists.</li>
</ul>
</MudCardContent>
</MudCard>
</div>
</div>
</EditForm>
}
</MudContainer>
@@ -298,6 +262,7 @@
private List<NamedMediaItemViewModel> _televisionShows;
private List<NamedMediaItemViewModel> _televisionSeasons;
private List<NamedMediaItemViewModel> _artists;
private List<FillerPresetViewModel> _fillerPresets;
private ProgramScheduleItemEditViewModel _selectedItem;
@@ -318,17 +283,27 @@
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_artists = await _mediator.Send(new GetAllArtists())
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_fillerPresets = await _mediator.Send(new GetAllFillerPresets())
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
string name = string.Empty;
Option<ProgramScheduleViewModel> maybeSchedule = await _mediator.Send(new GetProgramScheduleById(Id));
maybeSchedule.IfSome(vm => name = vm.Name);
Option<IEnumerable<ProgramScheduleItemViewModel>> maybeResults = await _mediator.Send(new GetProgramScheduleItems(Id));
maybeResults.IfSome(items => _schedule = new ProgramScheduleItemsEditViewModel
foreach (IEnumerable<ProgramScheduleItemViewModel> items in maybeResults)
{
Name = name,
Items = items.Map(ProjectToEditViewModel).ToList()
});
_schedule = new ProgramScheduleItemsEditViewModel
{
Name = name,
Items = items.Map(ProjectToEditViewModel).ToList()
};
if (_schedule.Items.Count == 1)
{
_selectedItem = _schedule.Items.Head();
}
}
}
private ProgramScheduleItemEditViewModel ProjectToEditViewModel(ProgramScheduleItemViewModel item)
@@ -347,7 +322,12 @@
MediaItem = item.MediaItem,
PlaybackOrder = item.PlaybackOrder,
CustomTitle = item.CustomTitle,
GuideMode = item.GuideMode
GuideMode = item.GuideMode,
PreRollFiller = item.PreRollFiller,
MidRollFiller = item.MidRollFiller,
PostRollFiller = item.PostRollFiller,
TailFiller = item.TailFiller,
FallbackFiller = item.FallbackFiller
};
switch (item)
@@ -358,11 +338,6 @@
case ProgramScheduleItemDurationViewModel duration:
result.PlayoutDuration = duration.PlayoutDuration;
result.TailMode = duration.TailMode;
result.TailCollectionType = duration.TailCollectionType;
result.TailCollection = duration.TailCollection;
result.TailMultiCollection = duration.TailMultiCollection;
result.TailSmartCollection = duration.TailSmartCollection;
result.TailMediaItem = duration.TailMediaItem;
break;
}
@@ -422,6 +397,9 @@
private Task<IEnumerable<NamedMediaItemViewModel>> SearchArtists(string value) =>
_artists.Filter(s => s.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private Task<IEnumerable<FillerPresetViewModel>> SearchFillerPresets(FillerKind fillerKind, string value) =>
_fillerPresets.Filter(p => p.FillerKind == fillerKind && p.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private async Task SaveChanges()
{
var items = _schedule.Items.Map(item => new ReplaceProgramScheduleItem(
@@ -438,13 +416,13 @@
item.MultipleCount,
item.PlayoutDuration,
item.TailMode,
item.TailCollectionType,
item.TailCollection?.Id,
item.TailMultiCollection?.Id,
item.TailSmartCollection?.Id,
item.TailMediaItem?.MediaItemId,
item.CustomTitle,
item.GuideMode)).ToList();
item.GuideMode,
item.PreRollFiller?.Id,
item.MidRollFiller?.Id,
item.PostRollFiller?.Id,
item.TailFiller?.Id,
item.FallbackFiller?.Id)).ToList();
Seq<BaseError> errorMessages = await _mediator.Send(new ReplaceProgramScheduleItems(Id, items)).Map(e => e.LeftToSeq());
+1 -1
View File
@@ -77,7 +77,7 @@
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Start Time">
@(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic")
@(context.StartType == StartType.Fixed ? context.StartTime == null ? string.Empty : DateTime.Today.Add(context.StartTime.Value).ToShortTimeString() : "Dynamic")
</MudTd>
<MudTd DataLabel="Collection">@context.Name</MudTd>
<MudTd DataLabel="Playout Mode">@context.PlayoutMode</MudTd>
+1 -1
View File
@@ -197,7 +197,7 @@
DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule)
{
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, PlaybackOrder.Shuffle, null, null, TailMode.None, ProgramScheduleItemCollectionType.Collection, null, null, null, null, null, GuideMode.Normal));
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null));
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
}
}
+1 -1
View File
@@ -225,7 +225,7 @@
DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule)
{
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, PlaybackOrder.Shuffle, null, null, TailMode.None, ProgramScheduleItemCollectionType.Collection, null, null, null, null, null, GuideMode.Normal));
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null));
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
}
}
+1
View File
@@ -58,6 +58,7 @@
<MudNavGroup Title="Lists" Expanded="true">
<MudNavLink Href="/media/collections">Collections</MudNavLink>
<MudNavLink Href="/media/trakt/lists">Trakt Lists</MudNavLink>
<MudNavLink Href="/media/filler/presets">Filler Presets</MudNavLink>
</MudNavGroup>
<MudNavLink Href="/schedules">Schedules</MudNavLink>
<MudNavLink Href="/playouts">Playouts</MudNavLink>
@@ -0,0 +1,40 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.ViewModels;
using FluentValidation;
namespace ErsatzTV.Validators
{
public class FillerPresetEditViewModelValidator : AbstractValidator<FillerPresetEditViewModel>
{
public FillerPresetEditViewModelValidator()
{
RuleFor(fp => fp.Name).NotEmpty();
RuleFor(fp => fp.FillerKind).NotEqual(FillerKind.None);
When(
fp => fp.FillerKind != FillerKind.Fallback && fp.FillerKind != FillerKind.Tail,
() => RuleFor(fp => fp.FillerMode).NotEqual(FillerMode.None));
When(
fp => fp.FillerMode == FillerMode.Count,
() => RuleFor(fp => fp.Count).NotNull().GreaterThanOrEqualTo(0));
When(
fp => fp.FillerMode == FillerMode.Duration,
() => RuleFor(fp => fp.Duration).NotNull());
When(
fp => fp.CollectionType == ProgramScheduleItemCollectionType.Collection,
() => RuleFor(fp => fp.Collection).NotNull());
When(
fp => fp.CollectionType == ProgramScheduleItemCollectionType.MultiCollection,
() => RuleFor(fp => fp.MultiCollection).NotNull());
When(
fp => fp.CollectionType == ProgramScheduleItemCollectionType.SmartCollection,
() => RuleFor(fp => fp.SmartCollection).NotNull());
When(
fp => fp.CollectionType is ProgramScheduleItemCollectionType.Artist or ProgramScheduleItemCollectionType
.TelevisionShow or ProgramScheduleItemCollectionType.TelevisionSeason,
() => RuleFor(fp => fp.MediaItem).NotNull());
}
}
}
@@ -0,0 +1,94 @@
using System;
using ErsatzTV.Application.Filler.Commands;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using LanguageExt;
using MediatR;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.ViewModels
{
public class FillerPresetEditViewModel
{
private FillerKind _fillerKind;
private TimeSpan? _duration;
private FillerMode _fillerMode;
private int? _count;
public int Id { get; set; }
public string Name { get; set; }
public FillerKind FillerKind
{
get => _fillerKind;
set
{
_fillerKind = value;
if (_fillerKind is FillerKind.Fallback or FillerKind.Tail)
{
FillerMode = FillerMode.None;
}
}
}
public FillerMode FillerMode
{
get => FillerKind is FillerKind.Fallback or FillerKind.Tail ? FillerMode.None : _fillerMode;
set => _fillerMode = value;
}
public TimeSpan? Duration
{
get => FillerMode == FillerMode.Duration ? _duration : null;
set => _duration = value;
}
public int? Count
{
get => FillerMode == FillerMode.Count ? _count : null;
set => _count = value;
}
public int? PadToNearestMinute { get; set; }
public ProgramScheduleItemCollectionType CollectionType { get; set; }
public MediaCollectionViewModel Collection { get; set; }
public NamedMediaItemViewModel MediaItem { get; set; }
public MultiCollectionViewModel MultiCollection { get; set; }
public SmartCollectionViewModel SmartCollection { get; set; }
public IRequest<Either<BaseError, Unit>> ToEdit() =>
new UpdateFillerPreset(
Id,
Name,
FillerKind,
FillerMode,
Duration.Map(FixDuration),
Count,
PadToNearestMinute,
CollectionType,
Collection?.Id,
MediaItem?.MediaItemId,
MultiCollection?.Id,
SmartCollection?.Id);
public IRequest<Either<BaseError, Unit>> ToUpdate() =>
new CreateFillerPreset(
Name,
FillerKind,
FillerMode,
Duration.Map(FixDuration),
Count,
PadToNearestMinute,
CollectionType,
Collection?.Id,
MediaItem?.MediaItemId,
MultiCollection?.Id,
SmartCollection?.Id);
private static TimeSpan FixDuration(TimeSpan duration) =>
duration > TimeSpan.FromDays(1) ? duration.Subtract(TimeSpan.FromDays(1)) : duration;
}
}
@@ -2,6 +2,7 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using ErsatzTV.Annotations;
using ErsatzTV.Application.Filler;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
@@ -14,7 +15,6 @@ namespace ErsatzTV.ViewModels
private int? _multipleCount;
private TimeSpan? _playoutDuration;
private TimeSpan? _startTime;
private ProgramScheduleItemCollectionType _tailCollectionType;
public int Id { get; set; }
public int Index { get; set; }
@@ -59,6 +59,11 @@ namespace ErsatzTV.ViewModels
public MultiCollectionViewModel MultiCollection { get; set; }
public SmartCollectionViewModel SmartCollection { get; set; }
public NamedMediaItemViewModel MediaItem { get; set; }
public FillerPresetViewModel PreRollFiller { get; set; }
public FillerPresetViewModel MidRollFiller { get; set; }
public FillerPresetViewModel PostRollFiller { get; set; }
public FillerPresetViewModel TailFiller { get; set; }
public FillerPresetViewModel FallbackFiller { get; set; }
public string CollectionName => CollectionType switch
{
@@ -87,33 +92,6 @@ namespace ErsatzTV.ViewModels
public TailMode TailMode { get; set; }
public ProgramScheduleItemCollectionType TailCollectionType
{
get => _tailCollectionType;
set
{
if (_tailCollectionType != value)
{
_tailCollectionType = value;
TailCollection = null;
TailMultiCollection = null;
TailMediaItem = null;
TailSmartCollection = null;
OnPropertyChanged(nameof(TailCollection));
OnPropertyChanged(nameof(TailMultiCollection));
OnPropertyChanged(nameof(TailMediaItem));
OnPropertyChanged(nameof(TailSmartCollection));
}
}
}
public MediaCollectionViewModel TailCollection { get; set; }
public MultiCollectionViewModel TailMultiCollection { get; set; }
public SmartCollectionViewModel TailSmartCollection { get; set; }
public NamedMediaItemViewModel TailMediaItem { get; set; }
public string CustomTitle { get; set; }
public GuideMode GuideMode { get; set; }