From c6bfe8c80ba44c266bdc9513982a1c8ec91ee71b Mon Sep 17 00:00:00 2001 From: Ministorm3 <4474921+Ministorm3@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:47:33 -0400 Subject: [PATCH] fix: improve validation and error logging in playout failure paths - log and throw a clear error when an external json channel has no start time, in both channel data refresh and playout item lookup - log a warning for each unmet precondition when remote plex streaming cannot proceed (unknown server, no active connection, no auth token) - return a validation error when playout alternate schedule items are empty - log a warning when an unsupported playback order falls back to random Co-Authored-By: Claude Fable 5 --- .../Commands/RefreshChannelDataHandler.cs | 12 +++++-- ...acePlayoutAlternateScheduleItemsHandler.cs | 5 ++- ErsatzTV.Core/Scheduling/PlayoutBuilder.cs | 5 ++- .../ExternalJsonPlayoutItemProvider.cs | 34 +++++++++++++++++-- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs b/ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs index fb43cb66f..771a05964 100644 --- a/ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs +++ b/ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs @@ -1189,10 +1189,18 @@ public class RefreshChannelDataHandler : IRequestHandler // must deserialize channel from json foreach (ExternalJsonChannel channel in maybeChannel) { - // TODO: null start time should log and throw + if (string.IsNullOrWhiteSpace(channel.StartTime)) + { + _logger.LogError( + "External json channel in file {File} has no start time; unable to refresh channel data", + path); + + throw new InvalidOperationException( + $"External json channel in file {path} has no start time"); + } DateTimeOffset startTime = DateTimeOffset.Parse( - channel.StartTime ?? string.Empty, + channel.StartTime, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToLocalTime(); diff --git a/ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItemsHandler.cs b/ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItemsHandler.cs index 0cf6490a6..580558159 100644 --- a/ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItemsHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItemsHandler.cs @@ -20,7 +20,10 @@ public class ReplacePlayoutAlternateScheduleItemsHandler( ReplacePlayoutAlternateScheduleItems request, CancellationToken cancellationToken) { - // TODO: validate that items is not empty + if (request.Items.Count == 0) + { + return BaseError.New("Playout alternate schedule items must not be empty"); + } try { diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index d95f60a09..c5eea25ee 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -1488,7 +1488,10 @@ public class PlayoutBuilder : IPlayoutBuilder goto default; default: - // TODO: handle this error case differently? + _logger.LogWarning( + "Unsupported playback order {PlaybackOrder}; falling back to random", + playbackOrder); + return new RandomizedMediaCollectionEnumerator(mediaItems, state); } } diff --git a/ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs b/ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs index 7e73234c4..28c7a216b 100644 --- a/ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs +++ b/ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs @@ -92,10 +92,18 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider // must deserialize channel from json foreach (ExternalJsonChannel channel in maybeChannel) { - // TODO: null start time should log and throw + if (string.IsNullOrWhiteSpace(channel.StartTime)) + { + _logger.LogError( + "External json channel in file {File} has no start time; unable to locate playout item", + playout.ScheduleFile); + + throw new InvalidOperationException( + $"External json channel in file {playout.ScheduleFile} has no start time"); + } DateTimeOffset startTime = DateTimeOffset.Parse( - channel.StartTime ?? string.Empty, + channel.StartTime, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToLocalTime(); @@ -207,14 +215,35 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider .Include(pms => pms.Connections) .SelectOneAsync(pms => pms.ServerName, pms => pms.ServerName == program.ServerKey, cancellationToken); + if (maybeServer.IsNone) + { + _logger.LogWarning( + "Unable to stream remotely; no Plex server found with server name {ServerName}", + program.ServerKey); + } + foreach (PlexMediaSource server in maybeServer) { Option maybeConnection = server.Connections.SingleOrDefault(c => c.IsActive); + if (maybeConnection.IsNone) + { + _logger.LogWarning( + "Unable to stream remotely; Plex server {ServerName} has no active connection", + server.ServerName); + } + foreach (PlexConnection connection in maybeConnection) { Option maybeToken = await _plexSecretStore.GetServerAuthToken(server.ClientIdentifier); + if (maybeToken.IsNone) + { + _logger.LogWarning( + "Unable to stream remotely; Plex server {ServerName} has no auth token", + server.ServerName); + } + foreach (PlexServerAuthToken token in maybeToken) { MediaItem mediaItem = program.Type switch @@ -230,7 +259,6 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider } } - // TODO: log errors? return new UnableToLocatePlayoutItem(); }