bug fixes (#1107)
* don't search an empty search index * fix bug with flood filler prediction check * extract subtitles on primary worker thread
This commit is contained in:
@@ -15,13 +15,13 @@ namespace ErsatzTV.Application.Channels;
|
|||||||
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
|
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
private readonly ChannelWriter<ISubtitleWorkerRequest> _ffmpegWorkerChannel;
|
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
||||||
|
|
||||||
public UpdateChannelHandler(
|
public UpdateChannelHandler(
|
||||||
ChannelWriter<ISubtitleWorkerRequest> ffmpegWorkerChannel,
|
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
||||||
IDbContextFactory<TvContext> dbContextFactory)
|
IDbContextFactory<TvContext> dbContextFactory)
|
||||||
{
|
{
|
||||||
_ffmpegWorkerChannel = ffmpegWorkerChannel;
|
_workerChannel = workerChannel;
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
|
|||||||
|
|
||||||
foreach (Playout playout in maybePlayout)
|
foreach (Playout playout in maybePlayout)
|
||||||
{
|
{
|
||||||
await _ffmpegWorkerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id));
|
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
namespace ErsatzTV.Application;
|
|
||||||
|
|
||||||
public interface ISubtitleWorkerRequest
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
|||||||
private readonly IClient _client;
|
private readonly IClient _client;
|
||||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
|
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
|
||||||
private readonly ChannelWriter<ISubtitleWorkerRequest> _ffmpegWorkerChannel;
|
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
||||||
private readonly IPlayoutBuilder _playoutBuilder;
|
private readonly IPlayoutBuilder _playoutBuilder;
|
||||||
|
|
||||||
public BuildPlayoutHandler(
|
public BuildPlayoutHandler(
|
||||||
@@ -25,13 +25,13 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
|||||||
IDbContextFactory<TvContext> dbContextFactory,
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
IPlayoutBuilder playoutBuilder,
|
IPlayoutBuilder playoutBuilder,
|
||||||
IFFmpegSegmenterService ffmpegSegmenterService,
|
IFFmpegSegmenterService ffmpegSegmenterService,
|
||||||
ChannelWriter<ISubtitleWorkerRequest> ffmpegWorkerChannel)
|
ChannelWriter<IBackgroundServiceRequest> workerChannel)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
_playoutBuilder = playoutBuilder;
|
_playoutBuilder = playoutBuilder;
|
||||||
_ffmpegSegmenterService = ffmpegSegmenterService;
|
_ffmpegSegmenterService = ffmpegSegmenterService;
|
||||||
_ffmpegWorkerChannel = ffmpegWorkerChannel;
|
_workerChannel = workerChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Either<BaseError, Unit>> Handle(BuildPlayout request, CancellationToken cancellationToken)
|
public async Task<Either<BaseError, Unit>> Handle(BuildPlayout request, CancellationToken cancellationToken)
|
||||||
@@ -56,7 +56,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
|||||||
_ffmpegSegmenterService.PlayoutUpdated(playout.Channel.Number);
|
_ffmpegSegmenterService.PlayoutUpdated(playout.Channel.Number);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _ffmpegWorkerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id));
|
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
namespace ErsatzTV.Application.Subtitles;
|
namespace ErsatzTV.Application.Subtitles;
|
||||||
|
|
||||||
public record ExtractEmbeddedSubtitles(Option<int> PlayoutId) : IRequest<Either<BaseError, Unit>>,
|
public record ExtractEmbeddedSubtitles(Option<int> PlayoutId) : IRequest<Either<BaseError, Unit>>,
|
||||||
ISubtitleWorkerRequest;
|
IBackgroundServiceRequest;
|
||||||
|
|||||||
@@ -737,17 +737,18 @@ public class PlayoutBuilder : IPlayoutBuilder
|
|||||||
(anchor.EnumeratorState = new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 }),
|
(anchor.EnumeratorState = new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 }),
|
||||||
() => new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 });
|
() => new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 });
|
||||||
|
|
||||||
if (await _mediaCollectionRepository.IsCustomPlaybackOrder(collectionKey.CollectionId ?? 0))
|
int collectionId = collectionKey.CollectionId ?? 0;
|
||||||
{
|
|
||||||
Option<Collection> collectionWithItems =
|
|
||||||
await _mediaCollectionRepository.GetCollectionWithCollectionItemsUntracked(
|
|
||||||
collectionKey.CollectionId ?? 0);
|
|
||||||
|
|
||||||
if (collectionKey.CollectionType == ProgramScheduleItemCollectionType.Collection &&
|
if (collectionKey.CollectionType == ProgramScheduleItemCollectionType.Collection &&
|
||||||
collectionWithItems.IsSome)
|
await _mediaCollectionRepository.IsCustomPlaybackOrder(collectionId))
|
||||||
|
{
|
||||||
|
Option<Collection> maybeCollectionWithItems =
|
||||||
|
await _mediaCollectionRepository.GetCollectionWithCollectionItemsUntracked(collectionId);
|
||||||
|
|
||||||
|
foreach (Collection collectionWithItems in maybeCollectionWithItems)
|
||||||
{
|
{
|
||||||
return new CustomOrderCollectionEnumerator(
|
return new CustomOrderCollectionEnumerator(
|
||||||
collectionWithItems.ValueUnsafe(),
|
collectionWithItems,
|
||||||
mediaItems,
|
mediaItems,
|
||||||
state);
|
state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,15 +91,18 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
|
|||||||
AddFiller(nextState, collectionEnumerators, scheduleItem, playoutItem, itemChapters));
|
AddFiller(nextState, collectionEnumerators, scheduleItem, playoutItem, itemChapters));
|
||||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||||
|
|
||||||
DateTimeOffset actualEndTime = playoutItems.Max(p => p.FinishOffset);
|
if (playoutItems.Count > 0)
|
||||||
if (Math.Abs((itemEndTimeWithFiller - actualEndTime).TotalSeconds) > 1)
|
|
||||||
{
|
{
|
||||||
_logger.LogWarning(
|
DateTimeOffset actualEndTime = playoutItems.Max(p => p.FinishOffset);
|
||||||
"Filler prediction failure: predicted {PredictedDuration} doesn't match actual {ActualDuration}",
|
if (Math.Abs((itemEndTimeWithFiller - actualEndTime).TotalSeconds) > 1)
|
||||||
itemEndTimeWithFiller,
|
{
|
||||||
actualEndTime);
|
_logger.LogWarning(
|
||||||
|
"Filler prediction failure: predicted {PredictedDuration} doesn't match actual {ActualDuration}",
|
||||||
|
itemEndTimeWithFiller,
|
||||||
|
actualEndTime);
|
||||||
|
|
||||||
// _logger.LogWarning("Playout items: {@PlayoutItems}", playoutItems);
|
// _logger.LogWarning("Playout items: {@PlayoutItems}", playoutItems);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nextState = nextState with
|
nextState = nextState with
|
||||||
|
|||||||
@@ -185,8 +185,9 @@ public sealed class SearchIndex : ISearchIndex
|
|||||||
};
|
};
|
||||||
|
|
||||||
client.Breadcrumbs.Leave("SearchIndex.Search", BreadcrumbType.State, metadata);
|
client.Breadcrumbs.Leave("SearchIndex.Search", BreadcrumbType.State, metadata);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(searchQuery.Replace("*", string.Empty).Replace("?", string.Empty)))
|
if (string.IsNullOrWhiteSpace(searchQuery.Replace("*", string.Empty).Replace("?", string.Empty)) ||
|
||||||
|
_writer.MaxDoc == 0)
|
||||||
{
|
{
|
||||||
return new SearchResult(new List<SearchItem>(), 0);
|
return new SearchResult(new List<SearchItem>(), 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
using System.Threading.Channels;
|
|
||||||
using Bugsnag;
|
|
||||||
using ErsatzTV.Application;
|
|
||||||
using ErsatzTV.Application.Subtitles;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Services;
|
|
||||||
|
|
||||||
public class SubtitleWorkerService : BackgroundService
|
|
||||||
{
|
|
||||||
private readonly ChannelReader<ISubtitleWorkerRequest> _channel;
|
|
||||||
private readonly ILogger<SubtitleWorkerService> _logger;
|
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
|
||||||
|
|
||||||
public SubtitleWorkerService(
|
|
||||||
ChannelReader<ISubtitleWorkerRequest> channel,
|
|
||||||
IServiceScopeFactory serviceScopeFactory,
|
|
||||||
ILogger<SubtitleWorkerService> logger)
|
|
||||||
{
|
|
||||||
_channel = channel;
|
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Subtitle worker service started");
|
|
||||||
|
|
||||||
await foreach (ISubtitleWorkerRequest request in _channel.ReadAllAsync(cancellationToken))
|
|
||||||
{
|
|
||||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
switch (request)
|
|
||||||
{
|
|
||||||
case ExtractEmbeddedSubtitles extractEmbeddedSubtitles:
|
|
||||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
|
||||||
await mediator.Send(extractEmbeddedSubtitles, cancellationToken);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to handle subtitle worker request");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
IClient client = scope.ServiceProvider.GetRequiredService<IClient>();
|
|
||||||
client.Notify(ex);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Subtitle worker service shutting down");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ using ErsatzTV.Application.MediaCollections;
|
|||||||
using ErsatzTV.Application.MediaSources;
|
using ErsatzTV.Application.MediaSources;
|
||||||
using ErsatzTV.Application.Playouts;
|
using ErsatzTV.Application.Playouts;
|
||||||
using ErsatzTV.Application.Search;
|
using ErsatzTV.Application.Search;
|
||||||
|
using ErsatzTV.Application.Subtitles;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Interfaces.Locking;
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -96,6 +97,9 @@ public class WorkerService : BackgroundService
|
|||||||
case MatchTraktListItems matchTraktListItems:
|
case MatchTraktListItems matchTraktListItems:
|
||||||
await mediator.Send(matchTraktListItems, cancellationToken);
|
await mediator.Send(matchTraktListItems, cancellationToken);
|
||||||
break;
|
break;
|
||||||
|
case ExtractEmbeddedSubtitles extractEmbeddedSubtitles:
|
||||||
|
await mediator.Send(extractEmbeddedSubtitles, cancellationToken);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested)
|
catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
|||||||
@@ -375,7 +375,6 @@ public class Startup
|
|||||||
AddChannel<IJellyfinBackgroundServiceRequest>(services);
|
AddChannel<IJellyfinBackgroundServiceRequest>(services);
|
||||||
AddChannel<IEmbyBackgroundServiceRequest>(services);
|
AddChannel<IEmbyBackgroundServiceRequest>(services);
|
||||||
AddChannel<IFFmpegWorkerRequest>(services);
|
AddChannel<IFFmpegWorkerRequest>(services);
|
||||||
AddChannel<ISubtitleWorkerRequest>(services);
|
|
||||||
AddChannel<ISearchIndexBackgroundServiceRequest>(services);
|
AddChannel<ISearchIndexBackgroundServiceRequest>(services);
|
||||||
|
|
||||||
services.AddScoped<IFFmpegVersionHealthCheck, FFmpegVersionHealthCheck>();
|
services.AddScoped<IFFmpegVersionHealthCheck, FFmpegVersionHealthCheck>();
|
||||||
@@ -470,7 +469,6 @@ public class Startup
|
|||||||
services.AddHostedService<EmbyService>();
|
services.AddHostedService<EmbyService>();
|
||||||
services.AddHostedService<JellyfinService>();
|
services.AddHostedService<JellyfinService>();
|
||||||
services.AddHostedService<PlexService>();
|
services.AddHostedService<PlexService>();
|
||||||
services.AddHostedService<SubtitleWorkerService>();
|
|
||||||
#endif
|
#endif
|
||||||
services.AddHostedService<FFmpegLocatorService>();
|
services.AddHostedService<FFmpegLocatorService>();
|
||||||
services.AddHostedService<WorkerService>();
|
services.AddHostedService<WorkerService>();
|
||||||
|
|||||||
Reference in New Issue
Block a user