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 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Playouts; |
||||
|
||||
public record GetPlayoutById(int PlayoutId) : IRequest<Option<PlayoutNameViewModel>>; |
@ -0,0 +1,32 @@
@@ -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 @@
@@ -1,3 +0,0 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record EraseBlockPlayoutHistory(int PlayoutId) : IRequest; |
@ -1,3 +0,0 @@
@@ -1,3 +0,0 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record EraseBlockPlayoutItems(int PlayoutId) : IRequest; |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record ErasePlayoutHistory(int PlayoutId) : IRequest; |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record ErasePlayoutItems(int PlayoutId) : IRequest; |
@ -0,0 +1,66 @@
@@ -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 @@
@@ -1,7 +1,12 @@
|
||||
using YamlDotNet.Serialization; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||
|
||||
public class YamlPlayoutContentItem |
||||
{ |
||||
public string Key { 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 @@
@@ -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 @@
@@ -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 @@
@@ -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