diff --git a/ErsatzTV.Application/ISearchBackgroundServiceRequest.cs b/ErsatzTV.Application/ISearchBackgroundServiceRequest.cs new file mode 100644 index 000000000..52bccad75 --- /dev/null +++ b/ErsatzTV.Application/ISearchBackgroundServiceRequest.cs @@ -0,0 +1,6 @@ +namespace ErsatzTV.Application +{ + public interface ISearchBackgroundServiceRequest + { + } +} diff --git a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs index f58a10790..543add0b3 100644 --- a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs +++ b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; +using ErsatzTV.Application.Search.Commands; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; @@ -26,6 +29,7 @@ namespace ErsatzTV.Application.MediaSources.Commands private readonly IMediator _mediator; private readonly IMovieFolderScanner _movieFolderScanner; private readonly IMusicVideoFolderScanner _musicVideoFolderScanner; + private readonly ChannelWriter _searchChannel; private readonly ITelevisionFolderScanner _televisionFolderScanner; public ScanLocalLibraryHandler( @@ -36,7 +40,8 @@ namespace ErsatzTV.Application.MediaSources.Commands IMusicVideoFolderScanner musicVideoFolderScanner, IEntityLocker entityLocker, IMediator mediator, - ILogger logger) + ILogger logger, + ChannelWriter searchChannel) { _libraryRepository = libraryRepository; _configElementRepository = configElementRepository; @@ -46,6 +51,7 @@ namespace ErsatzTV.Application.MediaSources.Commands _entityLocker = entityLocker; _mediator = mediator; _logger = logger; + _searchChannel = searchChannel; } public Task> Handle( @@ -87,7 +93,9 @@ namespace ErsatzTV.Application.MediaSources.Commands ffprobePath, lastScan, progressMin, - progressMax); + progressMax, + AddToSearchIndex, + RemoveFromSearchIndex); break; case LibraryMediaKind.Shows: await _televisionFolderScanner.ScanFolder( @@ -95,7 +103,9 @@ namespace ErsatzTV.Application.MediaSources.Commands ffprobePath, lastScan, progressMin, - progressMax); + progressMax, + AddToSearchIndex, + RemoveFromSearchIndex); break; case LibraryMediaKind.MusicVideos: await _musicVideoFolderScanner.ScanFolder( @@ -103,7 +113,9 @@ namespace ErsatzTV.Application.MediaSources.Commands ffprobePath, lastScan, progressMin, - progressMax); + progressMax, + AddToSearchIndex, + RemoveFromSearchIndex); break; } @@ -126,6 +138,12 @@ namespace ErsatzTV.Application.MediaSources.Commands return Unit.Default; } + private ValueTask AddToSearchIndex(List mediaItems) => + _searchChannel.WriteAsync(new AddItemsToSearchIndex(mediaItems)); + + private ValueTask RemoveFromSearchIndex(List mediaItemIds) => + _searchChannel.WriteAsync(new RemoveItemsFromSearchIndex(mediaItemIds)); + private async Task> Validate(IScanLocalLibrary request) => (await LocalLibraryMustExist(request), await ValidateFFprobePath()) .Apply( diff --git a/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs b/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs index ca98b9090..90b8401cb 100644 --- a/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; +using ErsatzTV.Application.Search.Commands; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; @@ -26,6 +29,7 @@ namespace ErsatzTV.Application.Plex.Commands private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner; private readonly IPlexSecretStore _plexSecretStore; private readonly IPlexTelevisionLibraryScanner _plexTelevisionLibraryScanner; + private readonly ChannelWriter _searchChannel; public SynchronizePlexLibraryByIdHandler( IMediaSourceRepository mediaSourceRepository, @@ -34,7 +38,8 @@ namespace ErsatzTV.Application.Plex.Commands IPlexTelevisionLibraryScanner plexTelevisionLibraryScanner, ILibraryRepository libraryRepository, IEntityLocker entityLocker, - ILogger logger) + ILogger logger, + ChannelWriter searchChannel) { _mediaSourceRepository = mediaSourceRepository; _plexSecretStore = plexSecretStore; @@ -43,6 +48,7 @@ namespace ErsatzTV.Application.Plex.Commands _libraryRepository = libraryRepository; _entityLocker = entityLocker; _logger = logger; + _searchChannel = searchChannel; } public Task> Handle( @@ -70,13 +76,17 @@ namespace ErsatzTV.Application.Plex.Commands await _plexMovieLibraryScanner.ScanLibrary( parameters.ConnectionParameters.ActiveConnection, parameters.ConnectionParameters.PlexServerAuthToken, - parameters.Library); + parameters.Library, + AddToSearchIndex, + RemoveFromSearchIndex); break; case LibraryMediaKind.Shows: await _plexTelevisionLibraryScanner.ScanLibrary( parameters.ConnectionParameters.ActiveConnection, parameters.ConnectionParameters.PlexServerAuthToken, - parameters.Library); + parameters.Library, + AddToSearchIndex, + RemoveFromSearchIndex); break; } @@ -94,6 +104,12 @@ namespace ErsatzTV.Application.Plex.Commands return Unit.Default; } + private ValueTask AddToSearchIndex(List mediaItems) => + _searchChannel.WriteAsync(new AddItemsToSearchIndex(mediaItems)); + + private ValueTask RemoveFromSearchIndex(List mediaItemIds) => + _searchChannel.WriteAsync(new RemoveItemsFromSearchIndex(mediaItemIds)); + private async Task> Validate(ISynchronizePlexLibraryById request) => (await ValidateConnection(request), await PlexLibraryMustExist(request)) .Apply( diff --git a/ErsatzTV.Application/Search/Commands/AddItemsToSearchIndex.cs b/ErsatzTV.Application/Search/Commands/AddItemsToSearchIndex.cs new file mode 100644 index 000000000..0ee5dbcf8 --- /dev/null +++ b/ErsatzTV.Application/Search/Commands/AddItemsToSearchIndex.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using ErsatzTV.Core.Domain; +using LanguageExt; + +namespace ErsatzTV.Application.Search.Commands +{ + public record AddItemsToSearchIndex(List MediaItems) : MediatR.IRequest, + ISearchBackgroundServiceRequest; +} diff --git a/ErsatzTV.Application/Search/Commands/AddItemsToSearchIndexHandler.cs b/ErsatzTV.Application/Search/Commands/AddItemsToSearchIndexHandler.cs new file mode 100644 index 000000000..cebbe509a --- /dev/null +++ b/ErsatzTV.Application/Search/Commands/AddItemsToSearchIndexHandler.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Core.Interfaces.Search; +using LanguageExt; + +namespace ErsatzTV.Application.Search.Commands +{ + public class AddItemsToSearchIndexHandler : MediatR.IRequestHandler + { + private readonly ISearchIndex _searchIndex; + + public AddItemsToSearchIndexHandler(ISearchIndex searchIndex) => _searchIndex = searchIndex; + + public Task Handle(AddItemsToSearchIndex request, CancellationToken cancellationToken) => + _searchIndex.AddItems(request.MediaItems); + } +} diff --git a/ErsatzTV.Application/Search/Commands/RebuildSearchIndex.cs b/ErsatzTV.Application/Search/Commands/RebuildSearchIndex.cs index 7eafb3e17..6b05213f9 100644 --- a/ErsatzTV.Application/Search/Commands/RebuildSearchIndex.cs +++ b/ErsatzTV.Application/Search/Commands/RebuildSearchIndex.cs @@ -2,5 +2,5 @@ namespace ErsatzTV.Application.Search.Commands { - public record RebuildSearchIndex : MediatR.IRequest, IBackgroundServiceRequest; + public record RebuildSearchIndex : MediatR.IRequest, ISearchBackgroundServiceRequest; } diff --git a/ErsatzTV.Application/Search/Commands/RemoveItemsFromSearchIndex.cs b/ErsatzTV.Application/Search/Commands/RemoveItemsFromSearchIndex.cs new file mode 100644 index 000000000..df010c7e7 --- /dev/null +++ b/ErsatzTV.Application/Search/Commands/RemoveItemsFromSearchIndex.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; +using LanguageExt; + +namespace ErsatzTV.Application.Search.Commands +{ + public record RemoveItemsFromSearchIndex(List MediaItemIds) : MediatR.IRequest, + ISearchBackgroundServiceRequest; +} diff --git a/ErsatzTV.Application/Search/Commands/RemoveItemsFromSearchIndexHandler.cs b/ErsatzTV.Application/Search/Commands/RemoveItemsFromSearchIndexHandler.cs new file mode 100644 index 000000000..dd24ccebd --- /dev/null +++ b/ErsatzTV.Application/Search/Commands/RemoveItemsFromSearchIndexHandler.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Core.Interfaces.Search; +using LanguageExt; + +namespace ErsatzTV.Application.Search.Commands +{ + public class RemoveItemsFromSearchIndexHandler : MediatR.IRequestHandler + { + private readonly ISearchIndex _searchIndex; + + public RemoveItemsFromSearchIndexHandler(ISearchIndex searchIndex) => _searchIndex = searchIndex; + + public Task Handle(RemoveItemsFromSearchIndex request, CancellationToken cancellationToken) => + _searchIndex.RemoveItems(request.MediaItemIds); + } +} diff --git a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs index 60856949d..77fd2d239 100644 --- a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs @@ -9,7 +9,6 @@ using ErsatzTV.Core.Errors; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Tests.Fakes; using FluentAssertions; @@ -88,7 +87,9 @@ namespace ErsatzTV.Core.Tests.Metadata FFprobePath, DateTimeOffset.MinValue, 0, - 1); + 1, + _ => ValueTask.CompletedTask, + _ => ValueTask.CompletedTask); result.IsLeft.Should().BeTrue(); result.IfLeft(error => error.Should().BeOfType()); @@ -113,7 +114,9 @@ namespace ErsatzTV.Core.Tests.Metadata FFprobePath, DateTimeOffset.MinValue, 0, - 1); + 1, + _ => ValueTask.CompletedTask, + _ => ValueTask.CompletedTask); result.IsRight.Should().BeTrue(); @@ -154,7 +157,10 @@ namespace ErsatzTV.Core.Tests.Metadata FFprobePath, DateTimeOffset.MinValue, 0, - 1); + 1, + _ => ValueTask.CompletedTask, + _ => ValueTask.CompletedTask); + result.IsRight.Should().BeTrue(); @@ -196,7 +202,10 @@ namespace ErsatzTV.Core.Tests.Metadata FFprobePath, DateTimeOffset.MinValue, 0, - 1); + 1, + _ => ValueTask.CompletedTask, + _ => ValueTask.CompletedTask); + result.IsRight.Should().BeTrue(); @@ -242,7 +251,10 @@ namespace ErsatzTV.Core.Tests.Metadata FFprobePath, DateTimeOffset.MinValue, 0, - 1); + 1, + _ => ValueTask.CompletedTask, + _ => ValueTask.CompletedTask); + result.IsRight.Should().BeTrue(); @@ -291,7 +303,10 @@ namespace ErsatzTV.Core.Tests.Metadata FFprobePath, DateTimeOffset.MinValue, 0, - 1); + 1, + _ => ValueTask.CompletedTask, + _ => ValueTask.CompletedTask); + result.IsRight.Should().BeTrue(); @@ -340,7 +355,10 @@ namespace ErsatzTV.Core.Tests.Metadata FFprobePath, DateTimeOffset.MinValue, 0, - 1); + 1, + _ => ValueTask.CompletedTask, + _ => ValueTask.CompletedTask); + result.IsRight.Should().BeTrue(); @@ -388,7 +406,10 @@ namespace ErsatzTV.Core.Tests.Metadata FFprobePath, DateTimeOffset.MinValue, 0, - 1); + 1, + _ => ValueTask.CompletedTask, + _ => ValueTask.CompletedTask); + result.IsRight.Should().BeTrue(); @@ -432,7 +453,10 @@ namespace ErsatzTV.Core.Tests.Metadata FFprobePath, DateTimeOffset.MinValue, 0, - 1); + 1, + _ => ValueTask.CompletedTask, + _ => ValueTask.CompletedTask); + result.IsRight.Should().BeTrue(); @@ -470,7 +494,10 @@ namespace ErsatzTV.Core.Tests.Metadata FFprobePath, DateTimeOffset.MinValue, 0, - 1); + 1, + _ => ValueTask.CompletedTask, + _ => ValueTask.CompletedTask); + result.IsRight.Should().BeTrue(); @@ -510,7 +537,9 @@ namespace ErsatzTV.Core.Tests.Metadata FFprobePath, DateTimeOffset.MinValue, 0, - 1); + 1, + _ => ValueTask.CompletedTask, + _ => ValueTask.CompletedTask); result.IsRight.Should().BeTrue(); @@ -539,7 +568,10 @@ namespace ErsatzTV.Core.Tests.Metadata FFprobePath, DateTimeOffset.MinValue, 0, - 1); + 1, + _ => ValueTask.CompletedTask, + _ => ValueTask.CompletedTask); + result.IsRight.Should().BeTrue(); @@ -556,7 +588,6 @@ namespace ErsatzTV.Core.Tests.Metadata _localMetadataProvider.Object, new Mock().Object, _imageCache.Object, - new Mock().Object, new Mock().Object, new Mock>().Object ); diff --git a/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs index 366993725..e0619c19e 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using LanguageExt; @@ -12,6 +13,8 @@ namespace ErsatzTV.Core.Interfaces.Metadata string ffprobePath, DateTimeOffset lastScan, decimal progressMin, - decimal progressMax); + decimal progressMax, + Func, ValueTask> addToSearchIndex, + Func, ValueTask> removeFromSearchIndex); } } diff --git a/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs index 1947c3f7f..f01fe2223 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using LanguageExt; @@ -12,6 +13,8 @@ namespace ErsatzTV.Core.Interfaces.Metadata string ffprobePath, DateTimeOffset lastScan, decimal progressMin, - decimal progressMax); + decimal progressMax, + Func, ValueTask> addToSearchIndex, + Func, ValueTask> removeFromSearchIndex); } } diff --git a/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs index f2d3d2b65..261d74a72 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using LanguageExt; @@ -12,6 +13,8 @@ namespace ErsatzTV.Core.Interfaces.Metadata string ffprobePath, DateTimeOffset lastScan, decimal progressMin, - decimal progressMax); + decimal progressMax, + Func, ValueTask> addToSearchIndex, + Func, ValueTask> removeFromSearchIndex); } } diff --git a/ErsatzTV.Core/Interfaces/Plex/IPlexMovieLibraryScanner.cs b/ErsatzTV.Core/Interfaces/Plex/IPlexMovieLibraryScanner.cs index f802b3347..2711c9314 100644 --- a/ErsatzTV.Core/Interfaces/Plex/IPlexMovieLibraryScanner.cs +++ b/ErsatzTV.Core/Interfaces/Plex/IPlexMovieLibraryScanner.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Plex; using LanguageExt; @@ -10,6 +12,8 @@ namespace ErsatzTV.Core.Interfaces.Plex Task> ScanLibrary( PlexConnection connection, PlexServerAuthToken token, - PlexLibrary plexMediaSourceLibrary); + PlexLibrary plexMediaSourceLibrary, + Func, ValueTask> addToSearchIndex, + Func, ValueTask> removeFromSearchIndex); } } diff --git a/ErsatzTV.Core/Interfaces/Plex/IPlexTelevisionLibraryScanner.cs b/ErsatzTV.Core/Interfaces/Plex/IPlexTelevisionLibraryScanner.cs index 2d2c19e86..f8dd7c0d2 100644 --- a/ErsatzTV.Core/Interfaces/Plex/IPlexTelevisionLibraryScanner.cs +++ b/ErsatzTV.Core/Interfaces/Plex/IPlexTelevisionLibraryScanner.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Plex; using LanguageExt; @@ -10,6 +12,8 @@ namespace ErsatzTV.Core.Interfaces.Plex Task> ScanLibrary( PlexConnection connection, PlexServerAuthToken token, - PlexLibrary plexMediaSourceLibrary); + PlexLibrary plexMediaSourceLibrary, + Func, ValueTask> addToSearchIndex, + Func, ValueTask> removeFromSearchIndex); } } diff --git a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs index 508485923..f86ae8736 100644 --- a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs @@ -8,7 +8,6 @@ using ErsatzTV.Core.Errors; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Search; using LanguageExt; using MediatR; using Microsoft.Extensions.Logging; @@ -25,7 +24,6 @@ namespace ErsatzTV.Core.Metadata private readonly ILogger _logger; private readonly IMediator _mediator; private readonly IMovieRepository _movieRepository; - private readonly ISearchIndex _searchIndex; public MovieFolderScanner( ILocalFileSystem localFileSystem, @@ -34,7 +32,6 @@ namespace ErsatzTV.Core.Metadata ILocalMetadataProvider localMetadataProvider, IMetadataRepository metadataRepository, IImageCache imageCache, - ISearchIndex searchIndex, IMediator mediator, ILogger logger) : base(localFileSystem, localStatisticsProvider, metadataRepository, imageCache, logger) @@ -42,7 +39,6 @@ namespace ErsatzTV.Core.Metadata _localFileSystem = localFileSystem; _movieRepository = movieRepository; _localMetadataProvider = localMetadataProvider; - _searchIndex = searchIndex; _mediator = mediator; _logger = logger; } @@ -52,7 +48,9 @@ namespace ErsatzTV.Core.Metadata string ffprobePath, DateTimeOffset lastScan, decimal progressMin, - decimal progressMax) + decimal progressMax, + Func, ValueTask> addToSearchIndex, + Func, ValueTask> removeFromSearchIndex) { decimal progressSpread = progressMax - progressMin; @@ -111,17 +109,7 @@ namespace ErsatzTV.Core.Metadata .BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt)); await maybeMovie.Match( - async result => - { - if (result.IsAdded) - { - await _searchIndex.AddItems(new List { result.Item }); - } - else if (result.IsUpdated) - { - await _searchIndex.UpdateItems(new List { result.Item }); - } - }, + async result => await addToSearchIndex(new List { result.Item }), error => { _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value); @@ -136,7 +124,7 @@ namespace ErsatzTV.Core.Metadata { _logger.LogInformation("Removing missing movie at {Path}", path); List ids = await _movieRepository.DeleteByPath(libraryPath, path); - await _searchIndex.RemoveItems(ids); + await removeFromSearchIndex(ids); } } diff --git a/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs b/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs index 4a7449682..4055465bc 100644 --- a/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs @@ -8,7 +8,6 @@ using ErsatzTV.Core.Errors; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Search; using LanguageExt; using MediatR; using Microsoft.Extensions.Logging; @@ -24,7 +23,6 @@ namespace ErsatzTV.Core.Metadata private readonly ILogger _logger; private readonly IMediator _mediator; private readonly IMusicVideoRepository _musicVideoRepository; - private readonly ISearchIndex _searchIndex; public MusicVideoFolderScanner( ILocalFileSystem localFileSystem, @@ -32,7 +30,6 @@ namespace ErsatzTV.Core.Metadata ILocalMetadataProvider localMetadataProvider, IMetadataRepository metadataRepository, IImageCache imageCache, - ISearchIndex searchIndex, IMusicVideoRepository musicVideoRepository, IMediator mediator, ILogger logger) : base( @@ -44,7 +41,6 @@ namespace ErsatzTV.Core.Metadata { _localFileSystem = localFileSystem; _localMetadataProvider = localMetadataProvider; - _searchIndex = searchIndex; _musicVideoRepository = musicVideoRepository; _mediator = mediator; _logger = logger; @@ -55,7 +51,9 @@ namespace ErsatzTV.Core.Metadata string ffprobePath, DateTimeOffset lastScan, decimal progressMin, - decimal progressMax) + decimal progressMax, + Func, ValueTask> addToSearchIndex, + Func, ValueTask> removeFromSearchIndex) { decimal progressSpread = progressMax - progressMin; @@ -104,17 +102,7 @@ namespace ErsatzTV.Core.Metadata .BindT(UpdateThumbnail); await maybeMusicVideo.Match( - async result => - { - if (result.IsAdded) - { - await _searchIndex.AddItems(new List { result.Item }); - } - else if (result.IsUpdated) - { - await _searchIndex.UpdateItems(new List { result.Item }); - } - }, + async result => await addToSearchIndex(new List { result.Item }), error => { _logger.LogWarning("Error processing music video at {Path}: {Error}", file, error.Value); @@ -129,7 +117,7 @@ namespace ErsatzTV.Core.Metadata { _logger.LogInformation("Removing missing music video at {Path}", path); List ids = await _musicVideoRepository.DeleteByPath(libraryPath, path); - await _searchIndex.RemoveItems(ids); + await removeFromSearchIndex(ids); } } diff --git a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs index 7c88c4047..feed66389 100644 --- a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs @@ -8,7 +8,6 @@ using ErsatzTV.Core.Errors; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Search; using LanguageExt; using MediatR; using Microsoft.Extensions.Logging; @@ -23,7 +22,6 @@ namespace ErsatzTV.Core.Metadata private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILogger _logger; private readonly IMediator _mediator; - private readonly ISearchIndex _searchIndex; private readonly ITelevisionRepository _televisionRepository; public TelevisionFolderScanner( @@ -33,7 +31,6 @@ namespace ErsatzTV.Core.Metadata ILocalMetadataProvider localMetadataProvider, IMetadataRepository metadataRepository, IImageCache imageCache, - ISearchIndex searchIndex, IMediator mediator, ILogger logger) : base( localFileSystem, @@ -45,7 +42,6 @@ namespace ErsatzTV.Core.Metadata _localFileSystem = localFileSystem; _televisionRepository = televisionRepository; _localMetadataProvider = localMetadataProvider; - _searchIndex = searchIndex; _mediator = mediator; _logger = logger; } @@ -55,7 +51,9 @@ namespace ErsatzTV.Core.Metadata string ffprobePath, DateTimeOffset lastScan, decimal progressMin, - decimal progressMax) + decimal progressMax, + Func, ValueTask> addToSearchIndex, + Func, ValueTask> removeFromSearchIndex) { decimal progressSpread = progressMax - progressMin; @@ -84,15 +82,7 @@ namespace ErsatzTV.Core.Metadata await maybeShow.Match( async result => { - if (result.IsAdded) - { - await _searchIndex.AddItems(new List { result.Item }); - } - else if (result.IsUpdated) - { - await _searchIndex.UpdateItems(new List { result.Item }); - } - + await addToSearchIndex(new List { result.Item }); await ScanSeasons( libraryPath, ffprobePath, @@ -122,7 +112,7 @@ namespace ErsatzTV.Core.Metadata await _televisionRepository.DeleteEmptySeasons(libraryPath); List ids = await _televisionRepository.DeleteEmptyShows(libraryPath); - await _searchIndex.RemoveItems(ids); + await removeFromSearchIndex(ids); return Unit.Default; } diff --git a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs index fffa6f694..9be3aaf0f 100644 --- a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Metadata; using LanguageExt; using MediatR; @@ -20,13 +20,11 @@ namespace ErsatzTV.Core.Plex private readonly IMetadataRepository _metadataRepository; private readonly IMovieRepository _movieRepository; private readonly IPlexServerApiClient _plexServerApiClient; - private readonly ISearchIndex _searchIndex; public PlexMovieLibraryScanner( IPlexServerApiClient plexServerApiClient, IMovieRepository movieRepository, IMetadataRepository metadataRepository, - ISearchIndex searchIndex, IMediator mediator, ILogger logger) : base(metadataRepository, logger) @@ -34,7 +32,6 @@ namespace ErsatzTV.Core.Plex _plexServerApiClient = plexServerApiClient; _movieRepository = movieRepository; _metadataRepository = metadataRepository; - _searchIndex = searchIndex; _mediator = mediator; _logger = logger; } @@ -42,7 +39,9 @@ namespace ErsatzTV.Core.Plex public async Task> ScanLibrary( PlexConnection connection, PlexServerAuthToken token, - PlexLibrary plexMediaSourceLibrary) + PlexLibrary plexMediaSourceLibrary, + Func, ValueTask> addToSearchIndex, + Func, ValueTask> removeFromSearchIndex) { Either> entries = await _plexServerApiClient.GetMovieLibraryContents( plexMediaSourceLibrary, @@ -65,17 +64,7 @@ namespace ErsatzTV.Core.Plex .BindT(existing => UpdateArtwork(existing, incoming)); await maybeMovie.Match( - async result => - { - if (result.IsAdded) - { - await _searchIndex.AddItems(new List { result.Item }); - } - else if (result.IsUpdated) - { - await _searchIndex.UpdateItems(new List { result.Item }); - } - }, + async result => await addToSearchIndex(new List { result.Item }), error => { _logger.LogWarning( @@ -88,7 +77,7 @@ namespace ErsatzTV.Core.Plex var movieKeys = movieEntries.Map(s => s.Key).ToList(); List ids = await _movieRepository.RemoveMissingPlexMovies(plexMediaSourceLibrary, movieKeys); - await _searchIndex.RemoveItems(ids); + await removeFromSearchIndex(ids); await _mediator.Publish(new LibraryScanProgress(plexMediaSourceLibrary.Id, 0)); }, diff --git a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs index 2179a5fc2..3c5f3baa5 100644 --- a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Metadata; using LanguageExt; using MediatR; @@ -20,14 +20,12 @@ namespace ErsatzTV.Core.Plex private readonly IMediator _mediator; private readonly IMetadataRepository _metadataRepository; private readonly IPlexServerApiClient _plexServerApiClient; - private readonly ISearchIndex _searchIndex; private readonly ITelevisionRepository _televisionRepository; public PlexTelevisionLibraryScanner( IPlexServerApiClient plexServerApiClient, ITelevisionRepository televisionRepository, IMetadataRepository metadataRepository, - ISearchIndex searchIndex, IMediator mediator, ILogger logger) : base(metadataRepository, logger) @@ -35,7 +33,6 @@ namespace ErsatzTV.Core.Plex _plexServerApiClient = plexServerApiClient; _televisionRepository = televisionRepository; _metadataRepository = metadataRepository; - _searchIndex = searchIndex; _mediator = mediator; _logger = logger; } @@ -43,7 +40,9 @@ namespace ErsatzTV.Core.Plex public async Task> ScanLibrary( PlexConnection connection, PlexServerAuthToken token, - PlexLibrary plexMediaSourceLibrary) + PlexLibrary plexMediaSourceLibrary, + Func, ValueTask> addToSearchIndex, + Func, ValueTask> removeFromSearchIndex) { Either> entries = await _plexServerApiClient.GetShowLibraryContents( plexMediaSourceLibrary, @@ -67,15 +66,7 @@ namespace ErsatzTV.Core.Plex await maybeShow.Match( async result => { - if (result.IsAdded) - { - await _searchIndex.AddItems(new List { result.Item }); - } - else if (result.IsUpdated) - { - await _searchIndex.UpdateItems(new List { result.Item }); - } - + await addToSearchIndex(new List { result.Item }); await ScanSeasons(plexMediaSourceLibrary, result.Item, connection, token); }, error => @@ -91,7 +82,7 @@ namespace ErsatzTV.Core.Plex var showKeys = showEntries.Map(s => s.Key).ToList(); List ids = await _televisionRepository.RemoveMissingPlexShows(plexMediaSourceLibrary, showKeys); - await _searchIndex.RemoveItems(ids); + await removeFromSearchIndex(ids); await _mediator.Publish(new LibraryScanProgress(plexMediaSourceLibrary.Id, 0)); diff --git a/ErsatzTV/Pages/Movie.razor b/ErsatzTV/Pages/Movie.razor index 4b17aa961..f24323d95 100644 --- a/ErsatzTV/Pages/Movie.razor +++ b/ErsatzTV/Pages/Movie.razor @@ -19,7 +19,7 @@ @if (!string.IsNullOrWhiteSpace(_movie.Poster)) { movie poster }
diff --git a/ErsatzTV/Pages/MovieList.razor b/ErsatzTV/Pages/MovieList.razor index 2b21c9cf1..8394befa3 100644 --- a/ErsatzTV/Pages/MovieList.razor +++ b/ErsatzTV/Pages/MovieList.razor @@ -130,7 +130,10 @@ private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) { - List GetSortedItems() => _data.Cards.OrderBy(m => m.SortTitle).ToList(); + List GetSortedItems() + { + return _data.Cards.OrderBy(m => m.SortTitle).ToList(); + } SelectClicked(GetSortedItems, card, e); } diff --git a/ErsatzTV/Pages/MusicVideoList.razor b/ErsatzTV/Pages/MusicVideoList.razor index c90b0ef48..dda224e7f 100644 --- a/ErsatzTV/Pages/MusicVideoList.razor +++ b/ErsatzTV/Pages/MusicVideoList.razor @@ -131,7 +131,10 @@ private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) { - List GetSortedItems() => _data.Cards.OrderBy(m => m.SortTitle).ToList(); + List GetSortedItems() + { + return _data.Cards.OrderBy(m => m.SortTitle).ToList(); + } SelectClicked(GetSortedItems, card, e); } diff --git a/ErsatzTV/Pages/TelevisionEpisodeList.razor b/ErsatzTV/Pages/TelevisionEpisodeList.razor index 4959a5c5a..63385e472 100644 --- a/ErsatzTV/Pages/TelevisionEpisodeList.razor +++ b/ErsatzTV/Pages/TelevisionEpisodeList.razor @@ -29,7 +29,7 @@ @if (!string.IsNullOrWhiteSpace(_season.Poster)) { show poster }
diff --git a/ErsatzTV/Pages/TelevisionSeasonList.razor b/ErsatzTV/Pages/TelevisionSeasonList.razor index f64e6d216..650a91a77 100644 --- a/ErsatzTV/Pages/TelevisionSeasonList.razor +++ b/ErsatzTV/Pages/TelevisionSeasonList.razor @@ -27,7 +27,7 @@ @if (!string.IsNullOrWhiteSpace(_show.Poster)) { show poster }
diff --git a/ErsatzTV/Pages/TelevisionShowList.razor b/ErsatzTV/Pages/TelevisionShowList.razor index 6a6f124ea..285055352 100644 --- a/ErsatzTV/Pages/TelevisionShowList.razor +++ b/ErsatzTV/Pages/TelevisionShowList.razor @@ -130,7 +130,10 @@ private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) { - List GetSortedItems() => _data.Cards.OrderBy(m => m.SortTitle).ToList(); + List GetSortedItems() + { + return _data.Cards.OrderBy(m => m.SortTitle).ToList(); + } SelectClicked(GetSortedItems, card, e); } diff --git a/ErsatzTV/Services/SchedulerService.cs b/ErsatzTV/Services/SchedulerService.cs index 3ad2f1240..34d426224 100644 --- a/ErsatzTV/Services/SchedulerService.cs +++ b/ErsatzTV/Services/SchedulerService.cs @@ -24,6 +24,7 @@ namespace ErsatzTV.Services private readonly IEntityLocker _entityLocker; private readonly ILogger _logger; private readonly ChannelWriter _plexWorkerChannel; + private readonly ChannelWriter _searchWorkerChannel; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ChannelWriter _workerChannel; @@ -31,12 +32,14 @@ namespace ErsatzTV.Services IServiceScopeFactory serviceScopeFactory, ChannelWriter workerChannel, ChannelWriter plexWorkerChannel, + ChannelWriter searchWorkerChannel, IEntityLocker entityLocker, ILogger logger) { _serviceScopeFactory = serviceScopeFactory; _workerChannel = workerChannel; _plexWorkerChannel = plexWorkerChannel; + _searchWorkerChannel = searchWorkerChannel; _entityLocker = entityLocker; _logger = logger; } @@ -123,6 +126,6 @@ namespace ErsatzTV.Services } private ValueTask RebuildSearchIndex(CancellationToken cancellationToken) => - _workerChannel.WriteAsync(new RebuildSearchIndex(), cancellationToken); + _searchWorkerChannel.WriteAsync(new RebuildSearchIndex(), cancellationToken); } } diff --git a/ErsatzTV/Services/SearchIndexService.cs b/ErsatzTV/Services/SearchIndexService.cs new file mode 100644 index 000000000..b7de5de2f --- /dev/null +++ b/ErsatzTV/Services/SearchIndexService.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using ErsatzTV.Application; +using ErsatzTV.Application.Search.Commands; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.Services +{ + public class SearchIndexService : BackgroundService + { + private readonly ChannelReader _channel; + private readonly ILogger _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; + + public SearchIndexService( + ChannelReader channel, + IServiceScopeFactory serviceScopeFactory, + ILogger logger) + { + _channel = channel; + _serviceScopeFactory = serviceScopeFactory; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Search index service started"); + + await foreach (ISearchBackgroundServiceRequest request in _channel.ReadAllAsync(cancellationToken)) + { + try + { + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + + switch (request) + { + case RebuildSearchIndex rebuildSearchIndex: + await mediator.Send(rebuildSearchIndex, cancellationToken); + break; + case AddItemsToSearchIndex addItemsToSearchIndex: + await mediator.Send(addItemsToSearchIndex, cancellationToken); + break; + case RemoveItemsFromSearchIndex removeItemsFromSearchIndex: + await mediator.Send(removeItemsFromSearchIndex, cancellationToken); + break; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to process search index service request"); + } + } + } + } +} diff --git a/ErsatzTV/Shared/FragmentLetterAnchor.razor b/ErsatzTV/Shared/FragmentLetterAnchor.razor index 7af7fcc0b..1ee3daa6c 100644 --- a/ErsatzTV/Shared/FragmentLetterAnchor.razor +++ b/ErsatzTV/Shared/FragmentLetterAnchor.razor @@ -1,5 +1,4 @@ -@using ErsatzTV.Application.MediaCards -@using LanguageExt.UnsafeValueAccess +@using LanguageExt.UnsafeValueAccess @typeparam TCard @{ var letters = new System.Collections.Generic.HashSet(); } @@ -25,4 +24,4 @@ { @ChildContent } -} +} \ No newline at end of file diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index b25b73be7..e4814a922 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -187,6 +187,7 @@ namespace ErsatzTV services.AddSingleton(); AddChannel(services); AddChannel(services); + AddChannel(services); services.AddScoped(); services.AddScoped();