From e089b12c2b21f43dcfbf38398ec54d3c4e80eb9e Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:15:55 -0500 Subject: [PATCH] trakt list fixes (#2560) * show reset playout build failures * fix scheduling trakt list playlists that contain shows --- CHANGELOG.md | 2 ++ .../Commands/PreviewPlaylistPlayout.cs | 4 +++- .../Commands/PreviewPlaylistPlayoutHandler.cs | 8 +++---- .../Commands/TraktCommandBase.cs | 3 ++- .../Errors/SchedulingLoopEncountered.cs | 3 +++ ErsatzTV.Core/Scheduling/PlayoutBuilder.cs | 21 +++++++++++++++-- ErsatzTV/Pages/PlaylistEditor.razor | 20 +++++++++++++++- .../RunOnce/DatabaseCleanerService.cs | 23 +++++++++++++++++++ 8 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 ErsatzTV.Core/Errors/SchedulingLoopEncountered.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index fddc7a5b5..c94ffdb4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix NVIDIA overlaying text subtitles and permanent watermark on 10-bit content - Fix UI error adding deco - Fix UI error editing watermarks and graphics elements on blocks +- Fix showing playout build failure details when resetting a playout +- Fix scheduling auto-generated trakt list playlists that contain shows ### Changed - Do not use graphics engine for single, permanent watermark diff --git a/ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayout.cs b/ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayout.cs index 051f1234f..d3697a884 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayout.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayout.cs @@ -1,5 +1,7 @@ using ErsatzTV.Application.Scheduling; +using ErsatzTV.Core; namespace ErsatzTV.Application.MediaCollections; -public record PreviewPlaylistPlayout(ReplacePlaylistItems Data) : IRequest>; +public record PreviewPlaylistPlayout(ReplacePlaylistItems Data) + : IRequest>>; diff --git a/ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayoutHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayoutHandler.cs index c9905a494..a3fc948ae 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayoutHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayoutHandler.cs @@ -17,9 +17,9 @@ public class PreviewPlaylistPlayoutHandler( IDbContextFactory dbContextFactory, IMediaCollectionRepository mediaCollectionRepository, IPlayoutBuilder playoutBuilder) - : IRequestHandler> + : IRequestHandler>> { - public async Task> Handle( + public async Task>> Handle( PreviewPlaylistPlayout request, CancellationToken cancellationToken) { @@ -65,7 +65,7 @@ public class PreviewPlaylistPlayoutHandler( PlayoutBuildMode.Reset, cancellationToken); - return await buildResult.MatchAsync( + return await buildResult.MatchAsync>>( async result => { var maxItems = 0; @@ -121,7 +121,7 @@ public class PreviewPlaylistPlayoutHandler( return onceThrough.OrderBy(i => i.StartOffset).Map(Scheduling.Mapper.ProjectToViewModel).ToList(); }, - _ => []); + error => error); } private static ProgramScheduleItemFlood MapToScheduleItem(PreviewPlaylistPlayout request) => diff --git a/ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs b/ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs index ff06bd01f..4835f32b4 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs @@ -195,7 +195,8 @@ public abstract class TraktCommandBase Index = item.Rank, PlaylistId = list.Playlist.Id, Playlist = list.Playlist, - IncludeInProgramGuide = true + IncludeInProgramGuide = true, + PlaybackOrder = PlaybackOrder.Chronological }; await dbContext.PlaylistItems.AddAsync(playlistItem, cancellationToken); diff --git a/ErsatzTV.Core/Errors/SchedulingLoopEncountered.cs b/ErsatzTV.Core/Errors/SchedulingLoopEncountered.cs new file mode 100644 index 000000000..fa261964b --- /dev/null +++ b/ErsatzTV.Core/Errors/SchedulingLoopEncountered.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Core.Errors; + +public class SchedulingLoopEncountered() : BaseError("Scheduling loop encountered"); diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index defb4e3b7..0e8f3234b 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -1,6 +1,7 @@ using System.Reflection; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; +using ErsatzTV.Core.Errors; using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; @@ -296,7 +297,7 @@ public class PlayoutBuilder : IPlayoutBuilder TrimStart = false; } - await BuildPlayoutItems( + Either maybeResult = await BuildPlayoutItems( playout, referenceData, result, @@ -306,6 +307,16 @@ public class PlayoutBuilder : IPlayoutBuilder true, cancellationToken); + foreach (BaseError error in maybeResult.LeftToSeq()) + { + return error; + } + + foreach (PlayoutBuildResult r in maybeResult.RightToSeq()) + { + result = r; + } + // time shift on demand channel if needed if (referenceData.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand) { @@ -350,6 +361,12 @@ public class PlayoutBuilder : IPlayoutBuilder PlayoutReferenceData referenceData, CancellationToken cancellationToken) { + if (referenceData.ProgramSchedule.Items.Count == 0) + { + _logger.LogWarning("Playout {Playout}'s schedule has no schedule items", referenceData.Channel.Name); + return BaseError.New($"Playout {referenceData.Channel.Name}'s schedule has no schedule items"); + } + Map> collectionMediaItems = await GetCollectionMediaItems(referenceData, cancellationToken); if (collectionMediaItems.IsEmpty) @@ -802,7 +819,7 @@ public class PlayoutBuilder : IPlayoutBuilder "Failed to schedule beyond {Time}; aborting playout build - this can be caused by an impossible schedule, or by a bug", playoutBuilderState.CurrentTime); - throw new InvalidOperationException("Scheduling loop encountered"); + return new SchedulingLoopEncountered(); } // _logger.LogDebug("Playout time is {CurrentTime}", playoutBuilderState.CurrentTime); diff --git a/ErsatzTV/Pages/PlaylistEditor.razor b/ErsatzTV/Pages/PlaylistEditor.razor index e0d55ad80..1043d6fa2 100644 --- a/ErsatzTV/Pages/PlaylistEditor.razor +++ b/ErsatzTV/Pages/PlaylistEditor.razor @@ -312,6 +312,12 @@ } +else if (!string.IsNullOrWhiteSpace(_previewMessage)) +{ + Preview + + @_previewMessage +} else if (_previewItems is not null) { Preview @@ -348,6 +354,7 @@ else if (_previewItems is not null) private PlaylistItemsEditViewModel _playlist = new() { Items = [] }; private PlaylistItemEditViewModel _selectedItem; + private string _previewMessage; private List _previewItems; public void Dispose() @@ -577,7 +584,18 @@ else if (_previewItems is not null) private async Task PreviewPlayout() { _selectedItem = null; - _previewItems = await Mediator.Send(new PreviewPlaylistPlayout(GenerateReplaceRequest()), _cts.Token); + _previewMessage = null; + + Either> result = await Mediator.Send(new PreviewPlaylistPlayout(GenerateReplaceRequest()), _cts.Token); + foreach (var error in result.LeftToSeq()) + { + _previewMessage = error.ToString(); + } + + foreach (List items in result.RightToSeq()) + { + _previewItems = items; + } } private static void UpdateEPG(PlaylistItemEditViewModel context, bool includeInProgramGuide) => context.IncludeInProgramGuide = includeInProgramGuide; diff --git a/ErsatzTV/Services/RunOnce/DatabaseCleanerService.cs b/ErsatzTV/Services/RunOnce/DatabaseCleanerService.cs index 6a5f36c84..9bf54b1d6 100644 --- a/ErsatzTV/Services/RunOnce/DatabaseCleanerService.cs +++ b/ErsatzTV/Services/RunOnce/DatabaseCleanerService.cs @@ -32,6 +32,7 @@ public class DatabaseCleanerService( await DeleteInvalidMediaItems(dbContext); await GenerateFallbackMetadata(scope, dbContext, stoppingToken); await ResetExternalSubtitleState(dbContext, stoppingToken); + await RemoveOrphanedPlaylists(dbContext, stoppingToken); systemStartup.DatabaseIsCleaned(); @@ -90,4 +91,26 @@ public class DatabaseCleanerService( "UPDATE `Subtitle` SET `IsExtracted`=0 WHERE `Path` IS NULL", cancellationToken); } + + private static async Task RemoveOrphanedPlaylists(TvContext dbContext, CancellationToken cancellationToken) + { + // remove orphaned playlists + await dbContext.Playlists + .Where(p => p.IsSystem && !dbContext.TraktLists.Any(t => t.PlaylistId == p.Id)) + .ExecuteDeleteAsync(cancellationToken); + + // turn off "generate playlist" on trakt lists with no referenced playlist + await dbContext.TraktLists + .Where(t => t.Playlist == null && t.GeneratePlaylist) + .ExecuteUpdateAsync( + setters => setters.SetProperty(t => t.GeneratePlaylist, false), + cancellationToken); + + // set playback order when unset + await dbContext.PlaylistItems + .Where(pi => pi.Playlist.IsSystem && pi.PlaybackOrder == PlaybackOrder.None) + .ExecuteUpdateAsync( + setters => setters.SetProperty(p => p.PlaybackOrder, PlaybackOrder.Chronological), + cancellationToken); + } }