diff --git a/CHANGELOG.md b/CHANGELOG.md index 61ae5aca8..f2cc8b289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/). - 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 diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelByPlayoutId.cs b/ErsatzTV.Application/Channels/Queries/GetChannelByPlayoutId.cs new file mode 100644 index 000000000..569cd11a1 --- /dev/null +++ b/ErsatzTV.Application/Channels/Queries/GetChannelByPlayoutId.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Channels; + +public record GetChannelByPlayoutId(int PlayoutId) : IRequest>; diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelByPlayoutIdHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelByPlayoutIdHandler.cs new file mode 100644 index 000000000..bf5122dc2 --- /dev/null +++ b/ErsatzTV.Application/Channels/Queries/GetChannelByPlayoutIdHandler.cs @@ -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 dbContextFactory) + : IRequestHandler> +{ + public async Task> 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)); + } +} diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutIdHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutIdHandler.cs index 1413a35b9..a82d83b7c 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutIdHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutIdHandler.cs @@ -4,16 +4,12 @@ using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.Channels; -public class GetChannelNameByPlayoutIdHandler : IRequestHandler> +public class GetChannelNameByPlayoutIdHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> { - private readonly IDbContextFactory _dbContextFactory; - - public GetChannelNameByPlayoutIdHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - public async Task> 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) diff --git a/ErsatzTV.Application/Playouts/Commands/ResetAllPlayoutsHandler.cs b/ErsatzTV.Application/Playouts/Commands/ResetAllPlayoutsHandler.cs index 15c999dee..c99a127f6 100644 --- a/ErsatzTV.Application/Playouts/Commands/ResetAllPlayoutsHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/ResetAllPlayoutsHandler.cs @@ -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: diff --git a/ErsatzTV.Application/Scheduling/Commands/ErasePlayoutHistoryHandler.cs b/ErsatzTV.Application/Scheduling/Commands/ErasePlayoutHistoryHandler.cs index 4394a6c1a..6a686627d 100644 --- a/ErsatzTV.Application/Scheduling/Commands/ErasePlayoutHistoryHandler.cs +++ b/ErsatzTV.Application/Scheduling/Commands/ErasePlayoutHistoryHandler.cs @@ -15,13 +15,16 @@ public class ErasePlayoutHistoryHandler(IDbContextFactory dbContextFa Option 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 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); } } } diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index ae3f11a3c..4e7dbc8da 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -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 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 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 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 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 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 scheduleItem.MarathonShuffleItems, scheduleItem.MarathonBatchSize, randomStartPoint, + random, cancellationToken); collectionEnumerators.Add(collectionKey, enumerator); @@ -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 scheduleItem.MarathonShuffleItems, scheduleItem.MarathonBatchSize, randomStartPoint, + random, cancellationToken); collectionEnumerators.Add(key, enumerator); @@ -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 bool marathonShuffleItems, int? marathonBatchSize, bool randomStartPoint, + Random random, CancellationToken cancellationToken) { Option maybeAnchor = playout.ProgramScheduleAnchors @@ -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 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 state = new CollectionEnumeratorState { Seed = state.Seed, - Index = Random.Next(0, mediaItems.Count - 1) + Index = random.Next(0, mediaItems.Count - 1) }; } diff --git a/ErsatzTV/Pages/ClassicPlayoutEditor.razor b/ErsatzTV/Pages/ClassicPlayoutEditor.razor new file mode 100644 index 000000000..680358315 --- /dev/null +++ b/ErsatzTV/Pages/ClassicPlayoutEditor.razor @@ -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; + + + + +
+ + @_channelName - Classic Playout + + +
+ Alternate Schedules +
+ @if (_playoutMode is ChannelPlayoutMode.OnDemand) + { + + Edit Alternate Schedules + + } + else + { + + Edit Alternate Schedules + + } +
+ Maintenance + + +
+ Playout Items and History +
+ + Erase Items and History + +
+
+
+
+ +@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 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); + } + +} diff --git a/ErsatzTV/Pages/Playouts.razor b/ErsatzTV/Pages/Playouts.razor index 33ceffd06..b6ba0d239 100644 --- a/ErsatzTV/Pages/Playouts.razor +++ b/ErsatzTV/Pages/Playouts.razor @@ -128,25 +128,13 @@ @if (context.ScheduleKind == PlayoutScheduleKind.Classic) { - if (context.PlayoutMode is ChannelPlayoutMode.OnDemand) - { - - - - - } - else - { - - - - - } - - + + + + + @@ -210,7 +198,7 @@ Href="@($"playouts/block/{context.PlayoutId}")"> - + @@ -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) {