@page "/media/filler/presets/{Id:int}/edit" @page "/media/filler/presets/add" @using ErsatzTV.Application.Artists @using ErsatzTV.Application.Filler @using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaItems @using ErsatzTV.Application.Television @using ErsatzTV.Core.Domain.Filler @using ErsatzTV.Validators @using FluentValidation.Results @implements IDisposable @inject NavigationManager NavigationManager @inject ILogger Logger @inject ISnackbar Snackbar @inject IMediator Mediator @(IsEdit ? "Save Filler Preset" : "Add Filler Preset")
Filler Preset
Name
Kind
Pre-Roll Mid-Roll Post-Roll Tail Fallback
Mode
Duration Count Pad Random Count
Duration
Count
Pad To Nearest Minute
5 (:00, :05, :10, :15, :20, etc) 10 (:00, :10, :20, :30, :40, :50) 15 (:00, :15, :30, :45) 30 (:00, :30)
Allow Watermarks
Collection Type
Collection Television Show Television Season Artist Multi Collection Smart Collection @if (_model.FillerKind is not FillerKind.Fallback and not FillerKind.Tail) { Playlist }
@if (_model.CollectionType == CollectionType.Collection) {
Collection
@foreach (MediaCollectionViewModel collection in _mediaCollections) { @collection.Name }
} @if (_model.CollectionType == CollectionType.MultiCollection) {
Multi Collection
@foreach (MultiCollectionViewModel collection in _multiCollections) { @collection.Name }
} @if (_model.CollectionType == CollectionType.SmartCollection) {
Smart Collection
@foreach (SmartCollectionViewModel collection in _smartCollections) { @collection.Name }
} @if (_model.CollectionType == CollectionType.TelevisionShow) {
Television Show
@foreach (NamedMediaItemViewModel show in _televisionShows) { @show.Name }
} @if (_model.CollectionType == CollectionType.TelevisionSeason) {
Television Season
@foreach (NamedMediaItemViewModel season in _televisionSeasons) { @season.Name }
} @if (_model.CollectionType == CollectionType.Artist) {
Artist
@foreach (NamedMediaItemViewModel artist in _artists) { @artist.Name }
} @if (_model.CollectionType == CollectionType.Playlist) {
Playlist Group
@foreach (PlaylistGroupViewModel playlistGroup in _playlistGroups) { @playlistGroup.Name }
Playlist
@foreach (PlaylistViewModel playlist in _playlists) { @playlist.Name }
}
Use Chapters As Media Items
Schedule individual chapters instead of entire files
Expression
total_points: total number of potential mid-roll points
matched_points: number of mid-roll points that have already matched the expression
total_duration: total duration of the content, in seconds
total_progress: normalized position from 0 to 1
last_mid_filler: seconds since last mid-roll filler
remaining_duration: duration of the content after this mid-roll point, in seconds
point: the position of the mid-roll point, in seconds
num: the mid-roll point number, starting with 1
@code { private CancellationTokenSource _cts; [Parameter] public int Id { get; set; } private readonly FillerPresetEditViewModel _model = new(); private readonly FillerPresetEditViewModelValidator _validator = new(); private MudForm _form; private readonly List _mediaCollections = []; private readonly List _multiCollections = []; private readonly List _smartCollections = []; private readonly List _televisionShows = []; private readonly List _televisionSeasons = []; private readonly List _artists = []; private readonly List _playlistGroups = []; private readonly List _playlists = []; private PlaylistGroupViewModel _selectedPlaylistGroup; public void Dispose() { _cts?.Cancel(); _cts?.Dispose(); } protected override async Task OnParametersSetAsync() { _cts?.Cancel(); _cts?.Dispose(); _cts = new CancellationTokenSource(); var token = _cts.Token; try { _mediaCollections.AddRange( await Mediator.Send(new GetAllCollections(), token) .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList())); _multiCollections.AddRange( await Mediator.Send(new GetAllMultiCollections(), token) .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList())); _smartCollections.AddRange( await Mediator.Send(new GetAllSmartCollections(), token) .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList())); _televisionShows.AddRange( await Mediator.Send(new GetAllTelevisionShows(), token) .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList())); _televisionSeasons.AddRange( await Mediator.Send(new GetAllTelevisionSeasons(), token) .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList())); _artists.AddRange( await Mediator.Send(new GetAllArtists(), token) .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList())); _playlistGroups.AddRange(await Mediator.Send(new GetAllPlaylistGroups(), token)); if (IsEdit) { Option maybeFillerPreset = await Mediator.Send(new GetFillerPresetById(Id), token); 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.AllowWatermarks = fillerPreset.AllowWatermarks; _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; _model.Expression = fillerPreset.Expression; _model.UseChaptersAsMediaItems = fillerPreset.UseChaptersAsMediaItems; _model.Playlist = fillerPreset.Playlist; }); } else { _model.Name = "New Filler Preset"; _model.FillerKind = FillerKind.PreRoll; _model.FillerMode = FillerMode.Duration; } foreach (int playlistGroupId in Optional(_model.Playlist?.PlaylistGroupId)) { foreach (PlaylistGroupViewModel group in Optional(_playlistGroups.Find(g => g.Id == playlistGroupId))) { _selectedPlaylistGroup = group; await UpdatePlaylistGroupItems(group); } } } catch (OperationCanceledException) { // do nothing } } private bool IsEdit => Id != 0; private async Task UpdatePlaylistGroupItems(PlaylistGroupViewModel playlistGroup) { _selectedPlaylistGroup = playlistGroup; _playlists.Clear(); _playlists.AddRange(await Mediator.Send(new GetPlaylistsByPlaylistGroupId(playlistGroup.Id), _cts.Token)); } private async Task HandleSubmitAsync() { await _form.Validate(); ValidationResult result = await _validator.ValidateAsync(_model, _cts.Token); if (result.IsValid) { // Create/Update return different result types (Update -> Unit, Create -> CreateFillerPresetResult), // so send each on its own branch and reduce to the shared Left error sequence. Seq errorMessage = IsEdit ? (await Mediator.Send(_model.ToEdit(), _cts.Token)).LeftToSeq() : (await Mediator.Send(_model.ToUpdate(), _cts.Token)).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")); } } }