Browse Source

refresh classic playouts by default (#2647)

pull/2649/head
Jason Dove 2 months ago committed by GitHub
parent
commit
3cb84c2491
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 3
      ErsatzTV.Application/Channels/Queries/GetChannelByPlayoutId.cs
  3. 21
      ErsatzTV.Application/Channels/Queries/GetChannelByPlayoutIdHandler.cs
  4. 10
      ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutIdHandler.cs
  5. 8
      ErsatzTV.Application/Playouts/Commands/ResetAllPlayoutsHandler.cs
  6. 13
      ErsatzTV.Application/Scheduling/Commands/ErasePlayoutHistoryHandler.cs
  7. 30
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  8. 97
      ErsatzTV/Pages/ClassicPlayoutEditor.razor
  9. 39
      ErsatzTV/Pages/Playouts.razor

4
CHANGELOG.md

@ -22,7 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -22,7 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add MPEG-TS Script system
- This allows using something other than ffmpeg (e.g. streamlink) to concatenate segments back together when using MPEG-TS streaming mode
- Scripts live in config / scripts / mpegts
- Each script gets its own subfolder which contains an `mpegts.yml` definition and corresponding windows (powershell) and linux (bash) scripts
- Each script gets its own subfolder which contains an `mpegts.yml` definition and corresponding windows (batch) and linux (bash) scripts
- The global MPEG-TS script can be configured in **Settings** > **FFmpeg** > **Default MPEG-TS Script**
- Add `.avs` AviSynth Script support to all local libraries
- `.avs` was added as a valid extension, so they should behave the same any other video file
@ -82,6 +82,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -82,6 +82,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- This should improve performance of library scans
### Changed
- Classic playouts: `Refresh` classic playouts from playout list; do not `Reset` them
- This mode maintains progress; progress can be reset by editing the playout and clicking `Erase Items and History`
- Use smaller batch size for search index updates (100, down from 1000)
- This should help newly scanned items appear in the UI more quickly
- Replace favicon and logo in background image used for error streams

3
ErsatzTV.Application/Channels/Queries/GetChannelByPlayoutId.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelByPlayoutId(int PlayoutId) : IRequest<Option<ChannelViewModel>>;

21
ErsatzTV.Application/Channels/Queries/GetChannelByPlayoutIdHandler.cs

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Channels.Mapper;
namespace ErsatzTV.Application.Channels;
public class GetChannelByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetChannelByPlayoutId, Option<ChannelViewModel>>
{
public async Task<Option<ChannelViewModel>> Handle(
GetChannelByPlayoutId request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Playouts
.Include(p => p.Channel)
.ThenInclude(c => c.Artwork)
.SingleOrDefaultAsync(p => p.Id == request.PlayoutId, cancellationToken)
.Map(p => ProjectToViewModel(p.Channel, 1));
}
}

10
ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutIdHandler.cs

@ -4,16 +4,12 @@ using Microsoft.EntityFrameworkCore; @@ -4,16 +4,12 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Channels;
public class GetChannelNameByPlayoutIdHandler : IRequestHandler<GetChannelNameByPlayoutId, Option<string>>
public class GetChannelNameByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetChannelNameByPlayoutId, Option<string>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetChannelNameByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Option<string>> Handle(GetChannelNameByPlayoutId request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Playouts
.Include(p => p.Channel)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId, cancellationToken)

8
ErsatzTV.Application/Playouts/Commands/ResetAllPlayoutsHandler.cs

@ -22,6 +22,14 @@ public class ResetAllPlayoutsHandler( @@ -22,6 +22,14 @@ public class ResetAllPlayoutsHandler(
switch (playout.ScheduleKind)
{
case PlayoutScheduleKind.Classic:
if (!locker.IsPlayoutLocked(playout.Id))
{
await channel.WriteAsync(
new BuildPlayout(playout.Id, PlayoutBuildMode.Refresh),
cancellationToken);
}
break;
case PlayoutScheduleKind.Block:
case PlayoutScheduleKind.Sequential:
case PlayoutScheduleKind.Scripted:

13
ErsatzTV.Application/Scheduling/Commands/ErasePlayoutHistoryHandler.cs

@ -15,13 +15,16 @@ public class ErasePlayoutHistoryHandler(IDbContextFactory<TvContext> dbContextFa @@ -15,13 +15,16 @@ public class ErasePlayoutHistoryHandler(IDbContextFactory<TvContext> dbContextFa
Option<Playout> maybePlayout = await dbContext.Playouts
.Filter(p => p.ScheduleKind == PlayoutScheduleKind.Block ||
p.ScheduleKind == PlayoutScheduleKind.Sequential ||
p.ScheduleKind == PlayoutScheduleKind.Scripted)
p.ScheduleKind == PlayoutScheduleKind.Scripted ||
p.ScheduleKind == PlayoutScheduleKind.Classic)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId, cancellationToken);
foreach (Playout playout in maybePlayout)
{
int nextSeed = new Random().Next();
playout.Seed = nextSeed;
playout.Anchor = null;
playout.OnDemandCheckpoint = null;
await dbContext.SaveChangesAsync(cancellationToken);
await dbContext.Database.ExecuteSqlAsync(
@ -31,6 +34,14 @@ public class ErasePlayoutHistoryHandler(IDbContextFactory<TvContext> dbContextFa @@ -31,6 +34,14 @@ public class ErasePlayoutHistoryHandler(IDbContextFactory<TvContext> dbContextFa
await dbContext.Database.ExecuteSqlAsync(
$"DELETE FROM PlayoutHistory WHERE PlayoutId = {playout.Id}",
cancellationToken);
await dbContext.Database.ExecuteSqlAsync(
$"DELETE FROM PlayoutAnchor WHERE PlayoutId = {playout.Id}",
cancellationToken);
await dbContext.Database.ExecuteSqlAsync(
$"DELETE FROM PlayoutProgramScheduleAnchor WHERE PlayoutId = {playout.Id}",
cancellationToken);
}
}
}

30
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -18,7 +18,6 @@ namespace ErsatzTV.Core.Scheduling; @@ -18,7 +18,6 @@ namespace ErsatzTV.Core.Scheduling;
// because the change happens during the playout
public class PlayoutBuilder : IPlayoutBuilder
{
private static readonly Random Random = new();
private readonly IArtistRepository _artistRepository;
private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalFileSystem _localFileSystem;
@ -196,6 +195,9 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -196,6 +195,9 @@ public class PlayoutBuilder : IPlayoutBuilder
var smartCollectionIds =
playout.ProgramScheduleAnchors.Map(a => Optional(a.SmartCollectionId)).Somes().ToHashSet();
var searchQueries =
playout.ProgramScheduleAnchors.Map(a => Optional(a.SearchQuery)).Somes().ToHashSet();
var rerunCollectionIds =
playout.ProgramScheduleAnchors.Map(a => Optional(a.RerunCollectionId)).Somes().ToHashSet();
@ -224,6 +226,13 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -224,6 +226,13 @@ public class PlayoutBuilder : IPlayoutBuilder
playout.ProgramScheduleAnchors.Add(minAnchor);
}
foreach (string searchQuery in searchQueries)
{
PlayoutProgramScheduleAnchor minAnchor = allAnchors.Filter(a => a.SearchQuery == searchQuery)
.MinBy(a => a.AnchorDateOffset.IfNone(DateTimeOffset.MaxValue).Ticks);
playout.ProgramScheduleAnchors.Add(minAnchor);
}
foreach (int rerunCollectionId in rerunCollectionIds)
{
PlayoutProgramScheduleAnchor minAnchor = allAnchors.Filter(a => a.RerunCollectionId == rerunCollectionId)
@ -290,6 +299,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -290,6 +299,7 @@ public class PlayoutBuilder : IPlayoutBuilder
playout.Anchor = null;
playout.ProgramScheduleAnchors.Clear();
playout.OnDemandCheckpoint = null;
playout.Seed = new Random().Next();
// don't trim start for on demand channels, we want to time shift it all forward
if (referenceData.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
@ -558,6 +568,8 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -558,6 +568,8 @@ public class PlayoutBuilder : IPlayoutBuilder
bool randomStartPoint,
CancellationToken cancellationToken)
{
var random = new Random(playout.Seed);
ProgramSchedule activeSchedule = PlayoutScheduleSelector.GetProgramScheduleFor(
referenceData.ProgramSchedule,
referenceData.ProgramScheduleAlternates,
@ -584,7 +596,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -584,7 +596,7 @@ public class PlayoutBuilder : IPlayoutBuilder
var sortedScheduleItems = activeSchedule.Items.OrderBy(i => i.Index).ToList();
CollectionEnumeratorState scheduleItemsEnumeratorState =
playout.Anchor?.ScheduleItemsEnumeratorState ?? new CollectionEnumeratorState
{ Seed = Random.Next(), Index = 0 };
{ Seed = random.Next(), Index = 0 };
IScheduleItemsEnumerator scheduleItemsEnumerator = activeSchedule.ShuffleScheduleItems
? new ShuffledScheduleItemsEnumerator(activeSchedule.Items, scheduleItemsEnumeratorState)
: new OrderedScheduleItemsEnumerator(activeSchedule.Items, scheduleItemsEnumeratorState);
@ -609,6 +621,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -609,6 +621,7 @@ public class PlayoutBuilder : IPlayoutBuilder
scheduleItem.MarathonShuffleItems,
scheduleItem.MarathonBatchSize,
randomStartPoint,
random,
cancellationToken);
collectionEnumerators.Add(collectionKey, enumerator);
@ -629,6 +642,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -629,6 +642,7 @@ public class PlayoutBuilder : IPlayoutBuilder
marathonShuffleItems: false,
marathonBatchSize: null,
randomStartPoint,
random,
cancellationToken);
collectionEnumerators.Add(collectionKey, enumerator);
@ -714,6 +728,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -714,6 +728,7 @@ public class PlayoutBuilder : IPlayoutBuilder
scheduleItem.MarathonShuffleItems,
scheduleItem.MarathonBatchSize,
randomStartPoint,
random,
cancellationToken);
collectionEnumerators.Add(key, enumerator);
@ -725,7 +740,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -725,7 +740,7 @@ public class PlayoutBuilder : IPlayoutBuilder
CollectionEnumeratorState enumeratorState =
playout.FillGroupIndices.Any(fgi => fgi.ProgramScheduleItemId == scheduleItem.Id)
? playout.FillGroupIndices.Find(fgi => fgi.ProgramScheduleItemId == scheduleItem.Id).EnumeratorState
: new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 };
: new CollectionEnumeratorState { Seed = random.Next() + scheduleItem.Id, Index = 0 };
switch (scheduleItem.FillWithGroupMode)
{
@ -1269,6 +1284,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -1269,6 +1284,7 @@ public class PlayoutBuilder : IPlayoutBuilder
bool marathonShuffleItems,
int? marathonBatchSize,
bool randomStartPoint,
Random random,
CancellationToken cancellationToken)
{
Option<PlayoutProgramScheduleAnchor> maybeAnchor = playout.ProgramScheduleAnchors
@ -1288,12 +1304,12 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -1288,12 +1304,12 @@ public class PlayoutBuilder : IPlayoutBuilder
{
// _logger.LogDebug("Selecting anchor {@Anchor}", anchor);
anchor.EnumeratorState ??= new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 };
anchor.EnumeratorState ??= new CollectionEnumeratorState { Seed = random.Next(), Index = 0 };
state = anchor.EnumeratorState;
}
state ??= new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 };
state ??= new CollectionEnumeratorState { Seed = random.Next(), Index = 0 };
if (collectionKey.CollectionType is CollectionType.RerunFirstRun or CollectionType.RerunRerun)
{
@ -1347,7 +1363,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -1347,7 +1363,7 @@ public class PlayoutBuilder : IPlayoutBuilder
state = new CollectionEnumeratorState
{
Seed = state.Seed,
Index = Random.Next(0, mediaItems.Count - 1)
Index = random.Next(0, mediaItems.Count - 1)
};
}
@ -1358,7 +1374,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -1358,7 +1374,7 @@ public class PlayoutBuilder : IPlayoutBuilder
state = new CollectionEnumeratorState
{
Seed = state.Seed,
Index = Random.Next(0, mediaItems.Count - 1)
Index = random.Next(0, mediaItems.Count - 1)
};
}

97
ErsatzTV/Pages/ClassicPlayoutEditor.razor

@ -0,0 +1,97 @@ @@ -0,0 +1,97 @@
@page "/playouts/classic/{Id:int}"
@using ErsatzTV.Application.Channels
@using ErsatzTV.Application.Scheduling
@implements IDisposable
@inject NavigationManager NavigationManager
@inject ISnackbar Snackbar
@inject IMediator Mediator
@inject IEntityLocker EntityLocker;
<MudForm Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
</MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">@_channelName - Classic Playout</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Alternate Schedules</MudText>
</div>
@if (_playoutMode is ChannelPlayoutMode.OnDemand)
{
<MudButton Disabled="@true" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Edit">
Edit Alternate Schedules
</MudButton>
}
else
{
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Primary" Href="@($"playouts/{Id}/alternate-schedules")" StartIcon="@Icons.Material.Filled.Edit">
Edit Alternate Schedules
</MudButton>
}
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Maintenance</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Playout Items and History</MudText>
</div>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Error" OnClick="@(_ => EraseItemsAndHistory())" StartIcon="@Icons.Material.Filled.Delete">
Erase Items and History
</MudButton>
</MudStack>
</MudContainer>
</div>
</MudForm>
@code {
private CancellationTokenSource _cts;
private ChannelPlayoutMode _playoutMode;
[Parameter]
public int Id { get; set; }
private string _channelName;
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
Option<ChannelViewModel> maybeChannel = await Mediator.Send(new GetChannelByPlayoutId(Id), token);
if (maybeChannel.IsNone)
{
NavigationManager.NavigateTo("playouts");
return;
}
foreach (ChannelViewModel channel in maybeChannel)
{
_channelName = channel.Name;
_playoutMode = channel.PlayoutMode;
}
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task EraseItemsAndHistory()
{
await Mediator.Send(new ErasePlayoutHistory(Id), _cts.Token);
Snackbar.Add("Erased playout items and history", Severity.Info);
}
}

39
ErsatzTV/Pages/Playouts.razor

@ -128,25 +128,13 @@ @@ -128,25 +128,13 @@
</div>
@if (context.ScheduleKind == PlayoutScheduleKind.Classic)
{
if (context.PlayoutMode is ChannelPlayoutMode.OnDemand)
{
<MudTooltip Text="Alternate Schedules are not supported with On Demand playout mode">
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar"
Disabled="true">
</MudIconButton>
</MudTooltip>
}
else
{
<MudTooltip Text="Edit Alternate Schedules">
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
Href="@($"playouts/{context.PlayoutId}/alternate-schedules")">
</MudIconButton>
</MudTooltip>
}
<MudTooltip Text="Reset Playout">
<MudTooltip Text="Edit Playout">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
Href="@($"playouts/classic/{context.PlayoutId}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Refresh Playout">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => ResetPlayout(context))">
@ -210,7 +198,7 @@ @@ -210,7 +198,7 @@
Href="@($"playouts/block/{context.PlayoutId}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Reset Playout">
<MudTooltip Text="Refresh Playout">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => ResetPlayout(context))">
@ -423,7 +411,16 @@ @@ -423,7 +411,16 @@
await Mediator.Send(new ResetAllPlayouts(), _cts.Token);
}
private async Task ResetPlayout(PlayoutNameViewModel playout) => await WorkerChannel.WriteAsync(new BuildPlayout(playout.PlayoutId, PlayoutBuildMode.Reset), _cts.Token);
private async Task ResetPlayout(PlayoutNameViewModel playout)
{
PlayoutBuildMode mode = playout.ScheduleKind switch
{
PlayoutScheduleKind.Classic => PlayoutBuildMode.Refresh,
_ => PlayoutBuildMode.Reset
};
await WorkerChannel.WriteAsync(new BuildPlayout(playout.PlayoutId, mode), _cts.Token);
}
private async Task ScheduleReset(PlayoutNameViewModel playout)
{

Loading…
Cancel
Save