mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* add multi_part; refactor skipping items * save and apply history for yaml playouts * do not remove history on yaml playout resetpull/1834/head
36 changed files with 12217 additions and 77 deletions
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Playouts; |
||||||
|
|
||||||
|
public record GetPlayoutById(int PlayoutId) : IRequest<Option<PlayoutNameViewModel>>; |
@ -0,0 +1,32 @@ |
|||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using ErsatzTV.Infrastructure.Extensions; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Playouts; |
||||||
|
|
||||||
|
public class GetPlayoutByIdHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||||
|
: IRequestHandler<GetPlayoutById, Option<PlayoutNameViewModel>> |
||||||
|
{ |
||||||
|
public async Task<Option<PlayoutNameViewModel>> Handle( |
||||||
|
GetPlayoutById request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||||
|
return await dbContext.Playouts |
||||||
|
.AsNoTracking() |
||||||
|
.Include(p => p.ProgramSchedule) |
||||||
|
.Include(p => p.Channel) |
||||||
|
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId) |
||||||
|
.MapT( |
||||||
|
p => new PlayoutNameViewModel( |
||||||
|
p.Id, |
||||||
|
p.ProgramSchedulePlayoutType, |
||||||
|
p.Channel.Name, |
||||||
|
p.Channel.Number, |
||||||
|
p.Channel.ProgressMode, |
||||||
|
p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name, |
||||||
|
p.TemplateFile, |
||||||
|
p.ExternalJsonFile, |
||||||
|
p.DailyRebuildTime)); |
||||||
|
} |
||||||
|
} |
@ -1,3 +0,0 @@ |
|||||||
namespace ErsatzTV.Application.Scheduling; |
|
||||||
|
|
||||||
public record EraseBlockPlayoutHistory(int PlayoutId) : IRequest; |
|
@ -1,3 +0,0 @@ |
|||||||
namespace ErsatzTV.Application.Scheduling; |
|
||||||
|
|
||||||
public record EraseBlockPlayoutItems(int PlayoutId) : IRequest; |
|
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record ErasePlayoutHistory(int PlayoutId) : IRequest; |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record ErasePlayoutItems(int PlayoutId) : IRequest; |
@ -0,0 +1,66 @@ |
|||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Domain.Scheduling; |
||||||
|
using ErsatzTV.Core.Interfaces.Scheduling; |
||||||
|
using ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||||
|
using Microsoft.Extensions.Logging; |
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; |
||||||
|
|
||||||
|
public class YamlPlayoutApplyHistoryHandler(EnumeratorCache enumeratorCache) |
||||||
|
{ |
||||||
|
public async Task<bool> Handle( |
||||||
|
YamlPlayoutContext context, |
||||||
|
YamlPlayoutContentItem contentItem, |
||||||
|
ILogger<YamlPlayoutBuilder> logger, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
if (string.IsNullOrWhiteSpace(contentItem.Key)) |
||||||
|
{ |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (!Enum.TryParse(contentItem.Order, true, out PlaybackOrder playbackOrder)) |
||||||
|
{ |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
Option<IMediaCollectionEnumerator> maybeEnumerator = await enumeratorCache.GetCachedEnumeratorForContent( |
||||||
|
context, |
||||||
|
contentItem.Key, |
||||||
|
cancellationToken); |
||||||
|
|
||||||
|
if (maybeEnumerator.IsNone) |
||||||
|
{ |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// check for playout history for this content
|
||||||
|
string historyKey = HistoryDetails.KeyForYamlContent(contentItem); |
||||||
|
|
||||||
|
DateTime historyTime = context.CurrentTime.UtcDateTime; |
||||||
|
Option<PlayoutHistory> maybeHistory = context.Playout.PlayoutHistory |
||||||
|
.Filter(h => h.Key == historyKey) |
||||||
|
.Filter(h => h.When < historyTime) |
||||||
|
.OrderByDescending(h => h.When) |
||||||
|
.HeadOrNone(); |
||||||
|
|
||||||
|
foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator) |
||||||
|
{ |
||||||
|
List<MediaItem> collectionItems = enumeratorCache.MediaItemsForContent(contentItem.Key); |
||||||
|
|
||||||
|
// seek to the appropriate place in the collection enumerator
|
||||||
|
foreach (PlayoutHistory h in maybeHistory) |
||||||
|
{ |
||||||
|
logger.LogDebug("History is applicable: {When}: {History}", h.When, h.Details); |
||||||
|
|
||||||
|
HistoryDetails.MoveToNextItem( |
||||||
|
collectionItems, |
||||||
|
h.Details, |
||||||
|
enumerator, |
||||||
|
playbackOrder); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
@ -1,7 +1,12 @@ |
|||||||
|
using YamlDotNet.Serialization; |
||||||
|
|
||||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||||
|
|
||||||
public class YamlPlayoutContentItem |
public class YamlPlayoutContentItem |
||||||
{ |
{ |
||||||
public string Key { get; set; } |
public string Key { get; set; } |
||||||
public string Order { get; set; } |
public string Order { get; set; } |
||||||
|
|
||||||
|
[YamlMember(Alias = "multi_part", ApplyNamingConventions = false)] |
||||||
|
public bool MultiPart { get; set; } |
||||||
} |
} |
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,41 @@ |
|||||||
|
using System; |
||||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||||
|
|
||||||
|
#nullable disable |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.MySql.Migrations |
||||||
|
{ |
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Add_PlayoutHistory_Finish : Migration |
||||||
|
{ |
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.AddColumn<DateTime>( |
||||||
|
name: "Finish", |
||||||
|
table: "PlayoutHistory", |
||||||
|
type: "datetime(6)", |
||||||
|
nullable: false, |
||||||
|
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); |
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>( |
||||||
|
name: "NextInstructionIndex", |
||||||
|
table: "PlayoutAnchor", |
||||||
|
type: "int", |
||||||
|
nullable: false, |
||||||
|
defaultValue: 0); |
||||||
|
} |
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.DropColumn( |
||||||
|
name: "Finish", |
||||||
|
table: "PlayoutHistory"); |
||||||
|
|
||||||
|
migrationBuilder.DropColumn( |
||||||
|
name: "NextInstructionIndex", |
||||||
|
table: "PlayoutAnchor"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,41 @@ |
|||||||
|
using System; |
||||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||||
|
|
||||||
|
#nullable disable |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||||
|
{ |
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Add_PlayoutHistory_Finish : Migration |
||||||
|
{ |
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.AddColumn<DateTime>( |
||||||
|
name: "Finish", |
||||||
|
table: "PlayoutHistory", |
||||||
|
type: "TEXT", |
||||||
|
nullable: false, |
||||||
|
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); |
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>( |
||||||
|
name: "NextInstructionIndex", |
||||||
|
table: "PlayoutAnchor", |
||||||
|
type: "INTEGER", |
||||||
|
nullable: false, |
||||||
|
defaultValue: 0); |
||||||
|
} |
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.DropColumn( |
||||||
|
name: "Finish", |
||||||
|
table: "PlayoutHistory"); |
||||||
|
|
||||||
|
migrationBuilder.DropColumn( |
||||||
|
name: "NextInstructionIndex", |
||||||
|
table: "PlayoutAnchor"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,122 @@ |
|||||||
|
@page "/playouts/yaml/{Id:int}" |
||||||
|
@using ErsatzTV.Application.Channels |
||||||
|
@using ErsatzTV.Application.Playouts |
||||||
|
@using ErsatzTV.Application.Scheduling |
||||||
|
@implements IDisposable |
||||||
|
@inject IDialogService Dialog |
||||||
|
@inject NavigationManager NavigationManager |
||||||
|
@inject ISnackbar Snackbar |
||||||
|
@inject IMediator Mediator |
||||||
|
@inject IEntityLocker EntityLocker; |
||||||
|
|
||||||
|
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> |
||||||
|
<MudText Typo="Typo.h4" Class="mb-4">Edit YAML Playout - @_channelName</MudText> |
||||||
|
<MudGrid> |
||||||
|
<MudItem xs="4"> |
||||||
|
<div style="max-width: 400px;" class="mr-4"> |
||||||
|
<MudCard> |
||||||
|
<MudCardHeader> |
||||||
|
<CardHeaderContent> |
||||||
|
<MudText Typo="Typo.h5">YAML File</MudText> |
||||||
|
</CardHeaderContent> |
||||||
|
</MudCardHeader> |
||||||
|
<MudCardContent> |
||||||
|
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => EditYamlFile())" Class="mt-4"> |
||||||
|
Edit YAML File |
||||||
|
</MudButton> |
||||||
|
</MudCardContent> |
||||||
|
</MudCard> |
||||||
|
</div> |
||||||
|
</MudItem> |
||||||
|
<MudItem xs="4"> |
||||||
|
<div style="max-width: 400px;" class="mb-6"> |
||||||
|
<MudCard> |
||||||
|
<MudCardHeader> |
||||||
|
<CardHeaderContent> |
||||||
|
<MudText Typo="Typo.h5">Playout Items and History</MudText> |
||||||
|
</CardHeaderContent> |
||||||
|
</MudCardHeader> |
||||||
|
<MudCardContent> |
||||||
|
<!-- reset will erase all items --> |
||||||
|
<!-- |
||||||
|
<div> |
||||||
|
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Warning" OnClick="@(_ => EraseItems(eraseHistory: false))" Class="mt-4"> |
||||||
|
Erase Items |
||||||
|
</MudButton> |
||||||
|
</div> |
||||||
|
--> |
||||||
|
<div> |
||||||
|
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Error" OnClick="@(_ => EraseItems(eraseHistory: true))" Class="mt-4"> |
||||||
|
Erase Items and History |
||||||
|
</MudButton> |
||||||
|
</div> |
||||||
|
</MudCardContent> |
||||||
|
</MudCard> |
||||||
|
</div> |
||||||
|
</MudItem> |
||||||
|
</MudGrid> |
||||||
|
</MudContainer> |
||||||
|
|
||||||
|
@code { |
||||||
|
private readonly CancellationTokenSource _cts = new(); |
||||||
|
private PlayoutNameViewModel _playout; |
||||||
|
|
||||||
|
[Parameter] |
||||||
|
public int Id { get; set; } |
||||||
|
|
||||||
|
private string _channelName; |
||||||
|
|
||||||
|
public void Dispose() |
||||||
|
{ |
||||||
|
_cts.Cancel(); |
||||||
|
_cts.Dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync() |
||||||
|
{ |
||||||
|
Option<string> maybeName = await Mediator.Send(new GetChannelNameByPlayoutId(Id), _cts.Token); |
||||||
|
if (maybeName.IsNone) |
||||||
|
{ |
||||||
|
NavigationManager.NavigateTo("/playouts"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
foreach (string name in maybeName) |
||||||
|
{ |
||||||
|
_channelName = name; |
||||||
|
} |
||||||
|
|
||||||
|
Option<PlayoutNameViewModel> maybePlayout = await Mediator.Send(new GetPlayoutById(Id), _cts.Token); |
||||||
|
foreach (PlayoutNameViewModel playout in maybePlayout) |
||||||
|
{ |
||||||
|
_playout = playout; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async Task EraseItems(bool eraseHistory) |
||||||
|
{ |
||||||
|
IRequest request = eraseHistory ? new ErasePlayoutHistory(Id) : new ErasePlayoutItems(Id); |
||||||
|
await Mediator.Send(request, _cts.Token); |
||||||
|
|
||||||
|
string message = eraseHistory ? "Erased playout items and history" : "Erased playout items"; |
||||||
|
Snackbar.Add(message, Severity.Info); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task EditYamlFile() |
||||||
|
{ |
||||||
|
if (_playout is null) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var parameters = new DialogParameters { { "YamlFile", $"{_playout.TemplateFile}" } }; |
||||||
|
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraLarge }; |
||||||
|
|
||||||
|
IDialogReference dialog = await Dialog.ShowAsync<EditYamlFileDialog>("Edit YAML File", parameters, options); |
||||||
|
DialogResult result = await dialog.Result; |
||||||
|
if (result is not null && !result.Canceled) |
||||||
|
{ |
||||||
|
await Mediator.Send(new UpdateYamlPlayout(_playout.PlayoutId, result.Data as string ?? _playout.TemplateFile), _cts.Token); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue