From d88e721d2f94682fbc58e172c4d3bf779a2a9e67 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:27:37 -0600 Subject: [PATCH] optimize database calls related to search index (#2645) --- CHANGELOG.md | 2 + .../Artists/Queries/GetArtistByIdHandler.cs | 22 +- .../Commands/MoveLocalLibraryPathHandler.cs | 10 +- .../Commands/AddTraktListHandler.cs | 7 +- .../Commands/DeleteTraktListHandler.cs | 12 +- .../Commands/MatchTraktListItemsHandler.cs | 6 +- .../Commands/TraktCommandBase.cs | 10 +- .../Movies/Queries/GetMovieByIdHandler.cs | 9 +- .../Commands/RebuildSearchIndexHandler.cs | 14 +- .../Commands/ReindexMediaItemsHandler.cs | 33 +- .../Queries/GetTelevisionShowByIdHandler.cs | 32 +- .../FFmpeg/FFmpegStreamSelectorTests.cs | 24 +- .../Scheduling/ScheduleIntegrationTests.cs | 9 +- ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs | 13 +- .../Interfaces/Metadata/ILanguageCodeCache.cs | 10 + .../Metadata/ILanguageCodeService.cs | 8 + .../Caching/ICachingSearchRepository.cs | 5 - .../Repositories/ISearchRepository.cs | 3 +- .../Interfaces/Search/ISearchIndex.cs | 10 +- ErsatzTV.Core/Metadata/LanguageCodeService.cs | 48 ++ .../Caching/CachingSearchRepositoryTests.cs | 33 - .../Caching/CachingSearchRepository.cs | 71 -- .../Data/Repositories/MediaItemRepository.cs | 27 +- .../Data/Repositories/SearchRepository.cs | 646 +++++++++--------- .../LanguageCodeQueryableExtensions.cs | 32 - .../Extensions/QueryableExtensions.cs | 132 ++++ .../Metadata/LanguageCodeCache.cs | 59 ++ .../Search/ElasticSearchIndex.cs | 137 ++-- .../Search/LuceneSearchIndex.cs | 127 ++-- ErsatzTV.Scanner/Program.cs | 5 +- .../RunOnce/RebuildSearchIndexService.cs | 5 + ErsatzTV/Startup.cs | 5 +- 32 files changed, 837 insertions(+), 729 deletions(-) create mode 100644 ErsatzTV.Core/Interfaces/Metadata/ILanguageCodeCache.cs create mode 100644 ErsatzTV.Core/Interfaces/Metadata/ILanguageCodeService.cs delete mode 100644 ErsatzTV.Core/Interfaces/Repositories/Caching/ICachingSearchRepository.cs create mode 100644 ErsatzTV.Core/Metadata/LanguageCodeService.cs delete mode 100644 ErsatzTV.Infrastructure.Tests/Data/Repositories/Caching/CachingSearchRepositoryTests.cs delete mode 100644 ErsatzTV.Infrastructure/Data/Repositories/Caching/CachingSearchRepository.cs create mode 100644 ErsatzTV.Infrastructure/Metadata/LanguageCodeCache.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 591855b8b..2e141cebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix bug where looping motion graphics wouldn't be displayed when seeking into second half of content - Fix `content_total_duration` value in graphics engine opacity expressions - This bug caused some graphics elements to display too early after first joining a channel +- Optimize database calls made for search index rebuilds and updates + - This should improve performance of library scans ### Changed - Use smaller batch size for search index updates (100, down from 1000) diff --git a/ErsatzTV.Application/Artists/Queries/GetArtistByIdHandler.cs b/ErsatzTV.Application/Artists/Queries/GetArtistByIdHandler.cs index 98daa4c2b..3c462d856 100644 --- a/ErsatzTV.Application/Artists/Queries/GetArtistByIdHandler.cs +++ b/ErsatzTV.Application/Artists/Queries/GetArtistByIdHandler.cs @@ -1,30 +1,26 @@ using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Artists.Mapper; namespace ErsatzTV.Application.Artists; -public class GetArtistByIdHandler : IRequestHandler> +public class GetArtistByIdHandler( + IArtistRepository artistRepository, + ISearchRepository searchRepository, + ILanguageCodeService languageCodeService) + : IRequestHandler> { - private readonly IArtistRepository _artistRepository; - private readonly ISearchRepository _searchRepository; - - public GetArtistByIdHandler(IArtistRepository artistRepository, ISearchRepository searchRepository) - { - _artistRepository = artistRepository; - _searchRepository = searchRepository; - } - public async Task> Handle( GetArtistById request, CancellationToken cancellationToken) { - Option maybeArtist = await _artistRepository.GetArtist(request.ArtistId); + Option maybeArtist = await artistRepository.GetArtist(request.ArtistId); return await maybeArtist.Match>>( async artist => { - List mediaCodes = await _searchRepository.GetLanguagesForArtist(artist); - List languageCodes = await _searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes); + List mediaCodes = await searchRepository.GetLanguagesForArtist(artist); + List languageCodes = languageCodeService.GetAllLanguageCodes(mediaCodes); return ProjectToViewModel(artist, languageCodes); }, () => Task.FromResult(Option.None)); diff --git a/ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPathHandler.cs b/ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPathHandler.cs index 699fc10e8..0a85786d7 100644 --- a/ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPathHandler.cs +++ b/ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPathHandler.cs @@ -2,7 +2,7 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories.Caching; +using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; @@ -15,20 +15,23 @@ public class MoveLocalLibraryPathHandler : IRequestHandler _dbContextFactory; private readonly IFallbackMetadataProvider _fallbackMetadataProvider; + private readonly ILanguageCodeService _languageCodeService; private readonly ILogger _logger; private readonly ISearchIndex _searchIndex; - private readonly ICachingSearchRepository _searchRepository; + private readonly ISearchRepository _searchRepository; public MoveLocalLibraryPathHandler( ISearchIndex searchIndex, - ICachingSearchRepository searchRepository, + ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, IDbContextFactory dbContextFactory, ILogger logger) { _searchIndex = searchIndex; _searchRepository = searchRepository; _fallbackMetadataProvider = fallbackMetadataProvider; + _languageCodeService = languageCodeService; _dbContextFactory = dbContextFactory; _logger = logger; } @@ -64,6 +67,7 @@ public class MoveLocalLibraryPathHandler : IRequestHandler dbContextFactory, ILogger logger, IEntityLocker entityLocker) - : base(traktApiClient, searchRepository, searchIndex, fallbackMetadataProvider, logger) + : base(traktApiClient, searchRepository, searchIndex, fallbackMetadataProvider, languageCodeService, logger) { _dbContextFactory = dbContextFactory; _entityLocker = entityLocker; diff --git a/ErsatzTV.Application/MediaCollections/Commands/DeleteTraktListHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/DeleteTraktListHandler.cs index 4bf59dbb4..5c509f7de 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/DeleteTraktListHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/DeleteTraktListHandler.cs @@ -2,7 +2,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories.Caching; +using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Infrastructure.Data; @@ -16,22 +16,25 @@ public class DeleteTraktListHandler : TraktCommandBase, IRequestHandler _dbContextFactory; private readonly IEntityLocker _entityLocker; private readonly IFallbackMetadataProvider _fallbackMetadataProvider; + private readonly ILanguageCodeService _languageCodeService; private readonly ISearchIndex _searchIndex; - private readonly ICachingSearchRepository _searchRepository; + private readonly ISearchRepository _searchRepository; public DeleteTraktListHandler( ITraktApiClient traktApiClient, - ICachingSearchRepository searchRepository, + ISearchRepository searchRepository, ISearchIndex searchIndex, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, IDbContextFactory dbContextFactory, ILogger logger, IEntityLocker entityLocker) - : base(traktApiClient, searchRepository, searchIndex, fallbackMetadataProvider, logger) + : base(traktApiClient, searchRepository, searchIndex, fallbackMetadataProvider, languageCodeService, logger) { _searchRepository = searchRepository; _searchIndex = searchIndex; _fallbackMetadataProvider = fallbackMetadataProvider; + _languageCodeService = languageCodeService; _dbContextFactory = dbContextFactory; _entityLocker = entityLocker; } @@ -65,6 +68,7 @@ public class DeleteTraktListHandler : TraktCommandBase, IRequestHandler dbContextFactory, ILogger logger, IEntityLocker entityLocker) : base( @@ -29,6 +30,7 @@ public class MatchTraktListItemsHandler : TraktCommandBase, searchRepository, searchIndex, fallbackMetadataProvider, + languageCodeService, logger) { _dbContextFactory = dbContextFactory; diff --git a/ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs b/ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs index 4835f32b4..a1cb8e5fb 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs @@ -1,7 +1,7 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories.Caching; +using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Core.Trakt; @@ -15,20 +15,23 @@ namespace ErsatzTV.Application.MediaCollections; public abstract class TraktCommandBase { private readonly IFallbackMetadataProvider _fallbackMetadataProvider; + private readonly ILanguageCodeService _languageCodeService; private readonly ILogger _logger; private readonly ISearchIndex _searchIndex; - private readonly ICachingSearchRepository _searchRepository; + private readonly ISearchRepository _searchRepository; protected TraktCommandBase( ITraktApiClient traktApiClient, - ICachingSearchRepository searchRepository, + ISearchRepository searchRepository, ISearchIndex searchIndex, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, ILogger logger) { _searchRepository = searchRepository; _searchIndex = searchIndex; _fallbackMetadataProvider = fallbackMetadataProvider; + _languageCodeService = languageCodeService; _logger = logger; TraktApiClient = traktApiClient; @@ -228,6 +231,7 @@ public abstract class TraktCommandBase await _searchIndex.RebuildItems( _searchRepository, _fallbackMetadataProvider, + _languageCodeService, ids.ToList(), cancellationToken); } diff --git a/ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs b/ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs index f084a740e..f7ad39f2f 100644 --- a/ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs +++ b/ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs @@ -2,10 +2,10 @@ using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Jellyfin; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data; -using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.Movies.Mapper; @@ -15,6 +15,7 @@ public class GetMovieByIdHandler : IRequestHandler _dbContextFactory; private readonly IEmbyPathReplacementService _embyPathReplacementService; + private readonly ILanguageCodeService _languageCodeService; private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService; private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMovieRepository _movieRepository; @@ -26,7 +27,8 @@ public class GetMovieByIdHandler : IRequestHandler> Handle( @@ -59,7 +62,7 @@ public class GetMovieByIdHandler : IRequestHandler ms.Language) .ToList(); - languageCodes.AddRange(await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCodes)); + languageCodes.AddRange(_languageCodeService.GetAllLanguageCodes(mediaCodes)); } foreach (Movie movie in maybeMovie) diff --git a/ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs b/ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs index 21ca4fa02..1978e6cf5 100644 --- a/ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs +++ b/ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs @@ -3,7 +3,6 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Repositories.Caching; using ErsatzTV.Core.Interfaces.Search; using Humanizer; using Microsoft.Extensions.Logging; @@ -14,18 +13,20 @@ public class RebuildSearchIndexHandler : IRequestHandler { private readonly IConfigElementRepository _configElementRepository; private readonly IFallbackMetadataProvider _fallbackMetadataProvider; + private readonly ILanguageCodeService _languageCodeService; private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; private readonly ISearchIndex _searchIndex; - private readonly ICachingSearchRepository _searchRepository; + private readonly ISearchRepository _searchRepository; private readonly SystemStartup _systemStartup; public RebuildSearchIndexHandler( ISearchIndex searchIndex, - ICachingSearchRepository searchRepository, + ISearchRepository searchRepository, IConfigElementRepository configElementRepository, ILocalFileSystem localFileSystem, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, SystemStartup systemStartup, ILogger logger) { @@ -35,6 +36,7 @@ public class RebuildSearchIndexHandler : IRequestHandler _configElementRepository = configElementRepository; _localFileSystem = localFileSystem; _fallbackMetadataProvider = fallbackMetadataProvider; + _languageCodeService = languageCodeService; _systemStartup = systemStartup; } @@ -58,7 +60,11 @@ public class RebuildSearchIndexHandler : IRequestHandler _logger.LogInformation("Migrating search index to version {Version}", _searchIndex.Version); var sw = Stopwatch.StartNew(); - await _searchIndex.Rebuild(_searchRepository, _fallbackMetadataProvider, cancellationToken); + await _searchIndex.Rebuild( + _searchRepository, + _fallbackMetadataProvider, + _languageCodeService, + cancellationToken); await _configElementRepository.Upsert( ConfigElementKey.SearchIndexVersion, diff --git a/ErsatzTV.Application/Search/Commands/ReindexMediaItemsHandler.cs b/ErsatzTV.Application/Search/Commands/ReindexMediaItemsHandler.cs index 7deae2d31..3e16704c4 100644 --- a/ErsatzTV.Application/Search/Commands/ReindexMediaItemsHandler.cs +++ b/ErsatzTV.Application/Search/Commands/ReindexMediaItemsHandler.cs @@ -1,28 +1,25 @@ using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories.Caching; +using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; namespace ErsatzTV.Application.Search; -public class ReindexMediaItemsHandler : IRequestHandler +public class ReindexMediaItemsHandler( + ISearchRepository searchRepository, + IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, + ISearchIndex searchIndex) + : IRequestHandler { - private readonly ICachingSearchRepository _cachingSearchRepository; - private readonly IFallbackMetadataProvider _fallbackMetadataProvider; - private readonly ISearchIndex _searchIndex; - - public ReindexMediaItemsHandler( - ICachingSearchRepository cachingSearchRepository, - IFallbackMetadataProvider fallbackMetadataProvider, - ISearchIndex searchIndex) - { - _cachingSearchRepository = cachingSearchRepository; - _fallbackMetadataProvider = fallbackMetadataProvider; - _searchIndex = searchIndex; - } - public async Task Handle(ReindexMediaItems request, CancellationToken cancellationToken) { - await _searchIndex.RebuildItems(_cachingSearchRepository, _fallbackMetadataProvider, request.MediaItemIds, cancellationToken); - _searchIndex.Commit(); + await searchIndex.RebuildItems( + searchRepository, + fallbackMetadataProvider, + languageCodeService, + request.MediaItemIds, + cancellationToken); + + searchIndex.Commit(); } } diff --git a/ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs index aea062c67..202058ce6 100644 --- a/ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs @@ -1,4 +1,5 @@ using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; @@ -7,27 +8,18 @@ using static ErsatzTV.Application.Television.Mapper; namespace ErsatzTV.Application.Television; -public class GetTelevisionShowByIdHandler : IRequestHandler> +public class GetTelevisionShowByIdHandler( + IDbContextFactory dbContextFactory, + ISearchRepository searchRepository, + ILanguageCodeService languageCodeService, + IMediaSourceRepository mediaSourceRepository) + : IRequestHandler> { - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ISearchRepository _searchRepository; - - public GetTelevisionShowByIdHandler( - IDbContextFactory dbContextFactory, - ISearchRepository searchRepository, - IMediaSourceRepository mediaSourceRepository) - { - _dbContextFactory = dbContextFactory; - _searchRepository = searchRepository; - _mediaSourceRepository = mediaSourceRepository; - } - public async Task> Handle( GetTelevisionShowById request, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); Option maybeShow = await dbContext.Shows .AsNoTracking() @@ -50,14 +42,14 @@ public class GetTelevisionShowByIdHandler : IRequestHandler maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin(cancellationToken) + Option maybeJellyfin = await mediaSourceRepository.GetAllJellyfin(cancellationToken) .Map(list => list.HeadOrNone()); - Option maybeEmby = await _mediaSourceRepository.GetAllEmby(cancellationToken) + Option maybeEmby = await mediaSourceRepository.GetAllEmby(cancellationToken) .Map(list => list.HeadOrNone()); - List mediaCodes = await _searchRepository.GetLanguagesForShow(show); - List languageCodes = await _searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes); + List mediaCodes = await searchRepository.GetLanguagesForShow(show); + List languageCodes = languageCodeService.GetAllLanguageCodes(mediaCodes); return ProjectToViewModel(show, languageCodes, maybeJellyfin, maybeEmby); } diff --git a/ErsatzTV.Core.Tests/FFmpeg/FFmpegStreamSelectorTests.cs b/ErsatzTV.Core.Tests/FFmpeg/FFmpegStreamSelectorTests.cs index 6ed53f0c4..c74dc1359 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/FFmpegStreamSelectorTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/FFmpegStreamSelectorTests.cs @@ -53,16 +53,16 @@ public class FFmpegStreamSelectorTests PreferredAudioLanguageCode = "eng" }; - ISearchRepository searchRepository = Substitute.For(); - searchRepository.GetAllThreeLetterLanguageCodes(Arg.Any>()) - .Returns(Task.FromResult(new List { "jpn" })); + ILanguageCodeService languageCodeService = Substitute.For(); + languageCodeService.GetAllLanguageCodes(Arg.Any>()) + .Returns(["jpn"]); var selector = new FFmpegStreamSelector( new ScriptEngine(Substitute.For>()), Substitute.For(), - searchRepository, Substitute.For(), Substitute.For(), + languageCodeService, Substitute.For>()); Option selectedStream = await selector.SelectAudioStream( @@ -115,16 +115,16 @@ public class FFmpegStreamSelectorTests PreferredAudioTitle = "Some" }; - ISearchRepository searchRepository = Substitute.For(); - searchRepository.GetAllThreeLetterLanguageCodes(Arg.Any>()) - .Returns(Task.FromResult(new List { "jpn", "eng" })); + ILanguageCodeService languageCodeService = Substitute.For(); + languageCodeService.GetAllLanguageCodes(Arg.Any>()) + .Returns(["jpn", "eng"]); var selector = new FFmpegStreamSelector( new ScriptEngine(Substitute.For>()), Substitute.For(), - searchRepository, Substitute.For(), Substitute.For(), + languageCodeService, Substitute.For>()); Option selectedStream = await selector.SelectAudioStream( @@ -165,16 +165,16 @@ public class FFmpegStreamSelectorTests var channel = new Channel(Guid.NewGuid()); - ISearchRepository searchRepository = Substitute.For(); - searchRepository.GetAllThreeLetterLanguageCodes(Arg.Any>()) - .Returns(Task.FromResult(new List { "heb" })); + ILanguageCodeService languageCodeService = Substitute.For(); + languageCodeService.GetAllLanguageCodes(Arg.Any>()) + .Returns(["heb"]); var selector = new FFmpegStreamSelector( new ScriptEngine(Substitute.For>()), Substitute.For(), - searchRepository, Substitute.For(), Substitute.For(), + languageCodeService, Substitute.For>()); Option selectedStream = await selector.SelectSubtitleStream( diff --git a/ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs b/ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs index b4c029215..3d3db22ce 100644 --- a/ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs @@ -5,15 +5,14 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Repositories.Caching; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data.Repositories; -using ErsatzTV.Infrastructure.Data.Repositories.Caching; using ErsatzTV.Infrastructure.Extensions; +using ErsatzTV.Infrastructure.Metadata; using ErsatzTV.Infrastructure.Search; using ErsatzTV.Infrastructure.Sqlite.Data; using LanguageExt.UnsafeValueAccess; @@ -86,11 +85,12 @@ public class ScheduleIntegrationTests services.AddSingleton((Func)(_ => new SerilogLoggerFactory())); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(_ => Substitute.For()); @@ -114,8 +114,9 @@ public class ScheduleIntegrationTests _cancellationToken); await searchIndex.Rebuild( - provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), + provider.GetRequiredService(), _cancellationToken); var builder = new PlayoutBuilder( diff --git a/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs b/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs index 27ed201f2..42c571c98 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs @@ -16,24 +16,24 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector { private readonly IConfigElementRepository _configElementRepository; private readonly ILocalFileSystem _localFileSystem; + private readonly ILanguageCodeService _languageCodeService; private readonly ILogger _logger; private readonly IScriptEngine _scriptEngine; - private readonly ISearchRepository _searchRepository; private readonly IStreamSelectorRepository _streamSelectorRepository; public FFmpegStreamSelector( IScriptEngine scriptEngine, IStreamSelectorRepository streamSelectorRepository, - ISearchRepository searchRepository, IConfigElementRepository configElementRepository, ILocalFileSystem localFileSystem, + ILanguageCodeService languageCodeService, ILogger logger) { _scriptEngine = scriptEngine; _streamSelectorRepository = streamSelectorRepository; - _searchRepository = searchRepository; _configElementRepository = configElementRepository; _localFileSystem = localFileSystem; + _languageCodeService = languageCodeService; _logger = logger; } @@ -73,8 +73,8 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector }); } - List allLanguageCodes = await _searchRepository.GetAllThreeLetterLanguageCodes([language]) - .Map(GetTwoAndThreeLetterLanguageCodes); + List allLanguageCodes = + GetTwoAndThreeLetterLanguageCodes(_languageCodeService.GetAllLanguageCodes([language])); if (allLanguageCodes.Count > 1) { _logger.LogDebug("Preferred audio language has multiple codes {Codes}", allLanguageCodes); @@ -190,8 +190,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector else { // filter to preferred language - allCodes = await _searchRepository.GetAllThreeLetterLanguageCodes([language]) - .Map(GetTwoAndThreeLetterLanguageCodes); + allCodes = GetTwoAndThreeLetterLanguageCodes(_languageCodeService.GetAllLanguageCodes([language])); if (allCodes.Count > 1) { _logger.LogDebug("Preferred subtitle language has multiple codes {Codes}", allCodes); diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILanguageCodeCache.cs b/ErsatzTV.Core/Interfaces/Metadata/ILanguageCodeCache.cs new file mode 100644 index 000000000..dfa7569d6 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Metadata/ILanguageCodeCache.cs @@ -0,0 +1,10 @@ +namespace ErsatzTV.Core.Interfaces.Metadata; + +public interface ILanguageCodeCache +{ + IReadOnlyDictionary CodeToGroupLookup { get; } + + IReadOnlyList AllGroups { get; } + + Task Load(CancellationToken cancellationToken); +} diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILanguageCodeService.cs b/ErsatzTV.Core/Interfaces/Metadata/ILanguageCodeService.cs new file mode 100644 index 000000000..8064b3978 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Metadata/ILanguageCodeService.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.Core.Interfaces.Metadata; + +public interface ILanguageCodeService +{ + List GetAllLanguageCodes(string mediaCode); + + List GetAllLanguageCodes(List mediaCodes); +} diff --git a/ErsatzTV.Core/Interfaces/Repositories/Caching/ICachingSearchRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/Caching/ICachingSearchRepository.cs deleted file mode 100644 index e92d2c646..000000000 --- a/ErsatzTV.Core/Interfaces/Repositories/Caching/ICachingSearchRepository.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace ErsatzTV.Core.Interfaces.Repositories.Caching; - -public interface ICachingSearchRepository : ISearchRepository, IDisposable -{ -} diff --git a/ErsatzTV.Core/Interfaces/Repositories/ISearchRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/ISearchRepository.cs index 3b1fbe87f..f4df1ffd5 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/ISearchRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/ISearchRepository.cs @@ -11,6 +11,5 @@ public interface ISearchRepository Task> GetSubLanguagesForSeason(Season season); Task> GetLanguagesForArtist(Artist artist); Task> GetSubLanguagesForArtist(Artist artist); - Task> GetAllThreeLetterLanguageCodes(List mediaCodes); - IAsyncEnumerable GetAllMediaItems(); + IAsyncEnumerable GetAllMediaItems(CancellationToken cancellationToken); } diff --git a/ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs b/ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs index 0731feb62..0684a07d5 100644 --- a/ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs +++ b/ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs @@ -2,7 +2,6 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Repositories.Caching; using ErsatzTV.Core.Search; namespace ErsatzTV.Core.Interfaces.Search; @@ -18,19 +17,22 @@ public interface ISearchIndex : IDisposable CancellationToken cancellationToken); Task Rebuild( - ICachingSearchRepository searchRepository, + ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, CancellationToken cancellationToken); Task RebuildItems( - ICachingSearchRepository searchRepository, + ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, IEnumerable itemIds, CancellationToken cancellationToken); Task UpdateItems( - ICachingSearchRepository searchRepository, + ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, List items); Task RemoveItems(IEnumerable ids); diff --git a/ErsatzTV.Core/Metadata/LanguageCodeService.cs b/ErsatzTV.Core/Metadata/LanguageCodeService.cs new file mode 100644 index 000000000..91dfc5603 --- /dev/null +++ b/ErsatzTV.Core/Metadata/LanguageCodeService.cs @@ -0,0 +1,48 @@ +using ErsatzTV.Core.Interfaces.Metadata; + +namespace ErsatzTV.Core.Metadata; + +public class LanguageCodeService(ILanguageCodeCache languageCodeCache) : ILanguageCodeService +{ + public List GetAllLanguageCodes(string mediaCode) + { + if (string.IsNullOrWhiteSpace(mediaCode)) + { + return []; + } + + string code = mediaCode.ToLowerInvariant(); + + if (languageCodeCache.CodeToGroupLookup.TryGetValue(code, out string[] group)) + { + return group.ToList(); + } + + return []; + } + + public List GetAllLanguageCodes(List mediaCodes) + { + var validCodes = mediaCodes + .Where(c => !string.IsNullOrWhiteSpace(c)) + .Select(c => c.ToLowerInvariant()) + .ToHashSet(); + + if (validCodes.Count == 0) + { + return []; + } + + var result = new System.Collections.Generic.HashSet(validCodes); + + foreach (string code in validCodes) + { + if (languageCodeCache.CodeToGroupLookup.TryGetValue(code, out string[] group)) + { + result.UnionWith(group); + } + } + + return result.ToList(); + } +} diff --git a/ErsatzTV.Infrastructure.Tests/Data/Repositories/Caching/CachingSearchRepositoryTests.cs b/ErsatzTV.Infrastructure.Tests/Data/Repositories/Caching/CachingSearchRepositoryTests.cs deleted file mode 100644 index 78137ccac..000000000 --- a/ErsatzTV.Infrastructure.Tests/Data/Repositories/Caching/CachingSearchRepositoryTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Infrastructure.Data.Repositories.Caching; -using LanguageExt; -using NSubstitute; -using NUnit.Framework; -using Shouldly; - -namespace ErsatzTV.Infrastructure.Tests.Data.Repositories.Caching; - -[TestFixture] -public class CachingSearchRepositoryTests -{ - [Test] - public async Task GetAllLanguageCodes_Should_Cache_Languages_Separately() - { - var englishMediaCodes = new List { "eng" }; - var frenchMediaCodes = new List { "fre" }; - var englishResult = new List { "english_result" }; - var frenchResult = new List { "french_result" }; - - ISearchRepository searchRepo = Substitute.For(); - searchRepo.GetAllThreeLetterLanguageCodes(englishMediaCodes).Returns(englishResult.AsTask()); - searchRepo.GetAllThreeLetterLanguageCodes(frenchMediaCodes).Returns(frenchResult.AsTask()); - - var repo = new CachingSearchRepository(searchRepo); - - List result1 = await repo.GetAllThreeLetterLanguageCodes(englishMediaCodes); - result1.ShouldBeEquivalentTo(englishResult); - - List result2 = await repo.GetAllThreeLetterLanguageCodes(frenchMediaCodes); - result2.ShouldBeEquivalentTo(frenchResult); - } -} diff --git a/ErsatzTV.Infrastructure/Data/Repositories/Caching/CachingSearchRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/Caching/CachingSearchRepository.cs deleted file mode 100644 index 9a3037436..000000000 --- a/ErsatzTV.Infrastructure/Data/Repositories/Caching/CachingSearchRepository.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.Concurrent; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Repositories.Caching; - -namespace ErsatzTV.Infrastructure.Data.Repositories.Caching; - -public class CachingSearchRepository : ICachingSearchRepository -{ - private readonly ConcurrentDictionary, List> _cache = new(); - private readonly ISearchRepository _searchRepository; - private readonly SemaphoreSlim _slim = new(1, 1); - private bool _disposedValue; - - public CachingSearchRepository(ISearchRepository searchRepository) => _searchRepository = searchRepository; - - public Task> GetItemToIndex(int id, CancellationToken cancellationToken) => - _searchRepository.GetItemToIndex(id, cancellationToken); - - public Task> GetLanguagesForShow(Show show) => _searchRepository.GetLanguagesForShow(show); - public Task> GetSubLanguagesForShow(Show show) => _searchRepository.GetSubLanguagesForShow(show); - - public Task> GetLanguagesForSeason(Season season) => _searchRepository.GetLanguagesForSeason(season); - - public Task> GetSubLanguagesForSeason(Season season) => - _searchRepository.GetSubLanguagesForSeason(season); - - public Task> GetLanguagesForArtist(Artist artist) => _searchRepository.GetLanguagesForArtist(artist); - - public Task> GetSubLanguagesForArtist(Artist artist) => - _searchRepository.GetSubLanguagesForArtist(artist); - - public async Task> GetAllThreeLetterLanguageCodes(List mediaCodes) - { - if (!_cache.ContainsKey(mediaCodes)) - { - await _slim.WaitAsync(); - try - { - _cache.TryAdd(mediaCodes, await _searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes)); - } - finally - { - _slim.Release(); - } - } - - return _cache[mediaCodes]; - } - - public IAsyncEnumerable GetAllMediaItems() => _searchRepository.GetAllMediaItems(); - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - _slim.Dispose(); - } - - _disposedValue = true; - } - } -} diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs index 5ee0139da..ceddf989b 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs @@ -4,6 +4,7 @@ using Dapper; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Extensions; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; using ErsatzTV.Infrastructure.Extensions; @@ -12,15 +13,13 @@ using Microsoft.Extensions.Logging; namespace ErsatzTV.Infrastructure.Data.Repositories; -public class MediaItemRepository : IMediaItemRepository +public class MediaItemRepository( + IDbContextFactory dbContextFactory, + ILanguageCodeService languageCodeService) : IMediaItemRepository { - private readonly IDbContextFactory _dbContextFactory; - - public MediaItemRepository(IDbContextFactory dbContextFactory) => _dbContextFactory = dbContextFactory; - public async Task> GetAllKnownCultures() { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); var result = new System.Collections.Generic.HashSet(); @@ -44,7 +43,7 @@ public class MediaItemRepository : IMediaItemRepository public async Task> GetAllLanguageCodesAndNames() { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); var result = new System.Collections.Generic.HashSet(); @@ -53,7 +52,7 @@ public class MediaItemRepository : IMediaItemRepository var unseenCodes = new System.Collections.Generic.HashSet(mediaCodes); foreach (string mediaCode in mediaCodes) { - foreach (string code in await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCode)) + foreach (string code in languageCodeService.GetAllLanguageCodes(mediaCode)) { Option maybeCulture = allCultures.Find(c => string.Equals( code, @@ -81,7 +80,7 @@ public class MediaItemRepository : IMediaItemRepository public async Task> FlagFileNotFound(LibraryPath libraryPath, string path) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); List ids = await dbContext.Connection.QueryAsync( @"SELECT M.Id @@ -101,7 +100,7 @@ public class MediaItemRepository : IMediaItemRepository public async Task> GetAllTrashedItems(LibraryPath libraryPath) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.QueryAsync( @"SELECT MF.Path FROM MediaItem M @@ -114,7 +113,7 @@ public class MediaItemRepository : IMediaItemRepository public async Task SetInterlacedRatio(MediaItem mediaItem, double interlacedRatio) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); var mediaVersion = mediaItem.GetHeadVersion(); mediaVersion.InterlacedRatio = interlacedRatio; @@ -126,7 +125,7 @@ public class MediaItemRepository : IMediaItemRepository public async Task FlagNormal(MediaItem mediaItem) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); mediaItem.State = MediaItemState.Normal; @@ -137,7 +136,7 @@ public class MediaItemRepository : IMediaItemRepository public async Task> DeleteItems(List mediaItemIds) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); foreach (int mediaItemId in mediaItemIds) { @@ -229,7 +228,7 @@ public class MediaItemRepository : IMediaItemRepository private async Task> GetAllLanguageCodes() { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.QueryAsync( @"SELECT LanguageCode FROM (SELECT Language AS LanguageCode diff --git a/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs index 04863d305..21129a4fa 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs @@ -1,4 +1,5 @@ -using Dapper; +using System.Runtime.CompilerServices; +using Dapper; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Extensions; @@ -6,183 +7,91 @@ using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Infrastructure.Data.Repositories; -public class SearchRepository : ISearchRepository +public class SearchRepository(IDbContextFactory dbContextFactory) : ISearchRepository { - private readonly IDbContextFactory _dbContextFactory; - - public SearchRepository(IDbContextFactory dbContextFactory) => _dbContextFactory = dbContextFactory; - public async Task> GetItemToIndex(int id, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.MediaItems + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + var baseItem = await dbContext.MediaItems .AsNoTracking() - .Include(mi => mi.Collections) - .Include(mi => mi.LibraryPath) - .ThenInclude(lp => lp.Library) - .Include(mi => (mi as Movie).MovieMetadata) - .ThenInclude(mm => mm.Genres) - .Include(mi => (mi as Movie).MovieMetadata) - .ThenInclude(mm => mm.Tags) - .Include(mi => (mi as Movie).MovieMetadata) - .ThenInclude(mm => mm.Studios) - .Include(mi => (mi as Movie).MovieMetadata) - .ThenInclude(mm => mm.Actors) - .Include(mi => (mi as Movie).MovieMetadata) - .ThenInclude(mm => mm.Directors) - .Include(mi => (mi as Movie).MovieMetadata) - .ThenInclude(mm => mm.Writers) - .Include(mi => (mi as Movie).MovieMetadata) - .ThenInclude(em => em.Guids) - .Include(mi => (mi as Movie).MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(mi => (mi as Movie).MediaVersions) - .ThenInclude(mm => mm.Streams) - .Include(mi => (mi as Episode).EpisodeMetadata) - .ThenInclude(em => em.Genres) - .Include(mi => (mi as Episode).EpisodeMetadata) - .ThenInclude(em => em.Tags) - .Include(mi => (mi as Episode).EpisodeMetadata) - .ThenInclude(em => em.Studios) - .Include(mi => (mi as Episode).EpisodeMetadata) - .ThenInclude(em => em.Actors) - .Include(mi => (mi as Episode).EpisodeMetadata) - .ThenInclude(em => em.Directors) - .Include(mi => (mi as Episode).EpisodeMetadata) - .ThenInclude(em => em.Writers) - .Include(mi => (mi as Episode).EpisodeMetadata) - .ThenInclude(em => em.Guids) - .Include(mi => (mi as Episode).MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(mi => (mi as Episode).MediaVersions) - .ThenInclude(em => em.Streams) - .Include(mi => (mi as Episode).MediaVersions) - .ThenInclude(em => em.MediaFiles) - .Include(mi => (mi as Episode).Season) - .ThenInclude(s => s.Show) - .ThenInclude(s => s.ShowMetadata) - .ThenInclude(s => s.Genres) - .Include(mi => (mi as Episode).Season) - .ThenInclude(s => s.Show) - .ThenInclude(s => s.ShowMetadata) - .ThenInclude(s => s.Tags) - .Include(mi => (mi as Episode).Season) - .ThenInclude(s => s.Show) - .ThenInclude(s => s.ShowMetadata) - .ThenInclude(s => s.Studios) - .Include(mi => (mi as Season).SeasonMetadata) - .ThenInclude(sm => sm.Genres) - .Include(mi => (mi as Season).SeasonMetadata) - .ThenInclude(sm => sm.Tags) - .Include(mi => (mi as Season).SeasonMetadata) - .ThenInclude(sm => sm.Studios) - .Include(mi => (mi as Season).SeasonMetadata) - .ThenInclude(sm => sm.Actors) - .Include(mi => (mi as Season).SeasonMetadata) - .ThenInclude(sm => sm.Guids) - .Include(mi => (mi as Season).Show) - .ThenInclude(sm => sm.ShowMetadata) - .ThenInclude(sm => sm.Genres) - .Include(mi => (mi as Season).Show) - .ThenInclude(sm => sm.ShowMetadata) - .ThenInclude(sm => sm.Tags) - .Include(mi => (mi as Season).Show) - .ThenInclude(sm => sm.ShowMetadata) - .ThenInclude(sm => sm.Studios) - .Include(mi => (mi as Show).ShowMetadata) - .ThenInclude(mm => mm.Genres) - .Include(mi => (mi as Show).ShowMetadata) - .ThenInclude(mm => mm.Tags) - .Include(mi => (mi as Show).ShowMetadata) - .ThenInclude(mm => mm.Studios) - .Include(mi => (mi as Show).ShowMetadata) - .ThenInclude(mm => mm.Actors) - .Include(mi => (mi as Show).ShowMetadata) - .ThenInclude(mm => mm.Guids) - .Include(mi => (mi as MusicVideo).Artist) - .ThenInclude(mm => mm.ArtistMetadata) - .Include(mi => (mi as MusicVideo).MusicVideoMetadata) - .ThenInclude(mm => mm.Artists) - .Include(mi => (mi as MusicVideo).MusicVideoMetadata) - .ThenInclude(mm => mm.Genres) - .Include(mi => (mi as MusicVideo).MusicVideoMetadata) - .ThenInclude(mm => mm.Tags) - .Include(mi => (mi as MusicVideo).MusicVideoMetadata) - .ThenInclude(mm => mm.Studios) - .Include(mi => (mi as MusicVideo).MusicVideoMetadata) - .ThenInclude(mm => mm.Guids) - .Include(mi => (mi as MusicVideo).MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(mi => (mi as MusicVideo).MediaVersions) - .ThenInclude(mm => mm.Streams) - .Include(mi => (mi as Artist).ArtistMetadata) - .ThenInclude(mm => mm.Genres) - .Include(mi => (mi as Artist).ArtistMetadata) - .ThenInclude(mm => mm.Styles) - .Include(mi => (mi as Artist).ArtistMetadata) - .ThenInclude(mm => mm.Moods) - .Include(mi => (mi as Artist).ArtistMetadata) - .ThenInclude(mm => mm.Guids) - .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Genres) - .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Tags) - .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Studios) - .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Actors) - .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Directors) - .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Writers) - .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Guids) - .Include(mi => (mi as OtherVideo).MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(mi => (mi as OtherVideo).MediaVersions) - .ThenInclude(ovm => ovm.Streams) - .Include(mi => (mi as Song).SongMetadata) - .ThenInclude(mm => mm.Tags) - .Include(mi => (mi as Song).SongMetadata) - .ThenInclude(mm => mm.Genres) - .Include(mi => (mi as Song).SongMetadata) - .ThenInclude(mm => mm.Guids) - .Include(mi => (mi as Song).MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(mi => (mi as Song).MediaVersions) - .ThenInclude(mm => mm.Streams) - .Include(mi => (mi as Image).ImageMetadata) - .ThenInclude(mm => mm.Tags) - .Include(mi => (mi as Image).ImageMetadata) - .ThenInclude(mm => mm.Genres) - .Include(mi => (mi as Image).ImageMetadata) - .ThenInclude(mm => mm.Guids) - .Include(mi => (mi as Image).MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(mi => (mi as Image).MediaVersions) - .ThenInclude(mm => mm.Streams) - .Include(mi => (mi as Image).MediaVersions) - .ThenInclude(mm => mm.MediaFiles) - .Include(mi => (mi as RemoteStream).RemoteStreamMetadata) - .ThenInclude(mm => mm.Tags) - .Include(mi => (mi as RemoteStream).RemoteStreamMetadata) - .ThenInclude(mm => mm.Genres) - .Include(mi => (mi as RemoteStream).RemoteStreamMetadata) - .ThenInclude(mm => mm.Guids) - .Include(mi => (mi as RemoteStream).MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(mi => (mi as RemoteStream).MediaVersions) - .ThenInclude(mm => mm.Streams) - .Include(mi => (mi as RemoteStream).MediaVersions) - .ThenInclude(mm => mm.MediaFiles) - .Include(mi => mi.TraktListItems) - .ThenInclude(tli => tli.TraktList) - .SelectOneAsync(mi => mi.Id, mi => mi.Id == id, cancellationToken); + .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); + + if (baseItem is null) + { + return Option.None; + } + + switch (baseItem) + { + case Movie: + return await dbContext.Movies + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); + case Episode: + return await dbContext.Episodes + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); + case Season: + return await dbContext.Seasons + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); + case Show: + return await dbContext.Shows + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); + case MusicVideo: + return await dbContext.MusicVideos + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); + case Artist: + return await dbContext.Artists + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); + case OtherVideo: + return await dbContext.OtherVideos + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); + case Song: + return await dbContext.Songs + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); + case Image: + return await dbContext.Images + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); + case RemoteStream: + return await dbContext.RemoteStreams + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); + } + + return Option.None; } public async Task> GetLanguagesForShow(Show show) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.QueryAsync( @"SELECT DISTINCT Language FROM MediaStream @@ -195,7 +104,7 @@ public class SearchRepository : ISearchRepository public async Task> GetSubLanguagesForShow(Show show) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.QueryAsync( @"SELECT DISTINCT Language FROM MediaStream @@ -208,7 +117,7 @@ public class SearchRepository : ISearchRepository public async Task> GetLanguagesForSeason(Season season) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.QueryAsync( @"SELECT DISTINCT Language FROM MediaStream @@ -220,7 +129,7 @@ public class SearchRepository : ISearchRepository public async Task> GetSubLanguagesForSeason(Season season) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.QueryAsync( @"SELECT DISTINCT Language FROM MediaStream @@ -232,7 +141,7 @@ public class SearchRepository : ISearchRepository public async Task> GetLanguagesForArtist(Artist artist) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.QueryAsync( @"SELECT DISTINCT Language FROM MediaStream @@ -245,7 +154,7 @@ public class SearchRepository : ISearchRepository public async Task> GetSubLanguagesForArtist(Artist artist) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.QueryAsync( @"SELECT DISTINCT Language FROM MediaStream @@ -256,177 +165,230 @@ public class SearchRepository : ISearchRepository new { ArtistId = artist.Id }).Map(result => result.ToList()); } - public virtual async Task> GetAllThreeLetterLanguageCodes(List mediaCodes) + public async IAsyncEnumerable GetAllMediaItems( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var item in GetAllMovies(cancellationToken)) + { + yield return item; + } + + await foreach (var item in GetAllShows(cancellationToken)) + { + yield return item; + } + + await foreach (var item in GetAllSeasons(cancellationToken)) + { + yield return item; + } + + await foreach (var item in GetAllEpisodes(cancellationToken)) + { + yield return item; + } + + await foreach (var item in GetAllMusicVideos(cancellationToken)) + { + yield return item; + } + + await foreach (var item in GetAllArtists(cancellationToken)) + { + yield return item; + } + + await foreach (var item in GetAllOtherVideos(cancellationToken)) + { + yield return item; + } + + await foreach (var item in GetAllSongs(cancellationToken)) + { + yield return item; + } + + await foreach (var item in GetAllImages(cancellationToken)) + { + yield return item; + } + + await foreach (var item in GetAllRemoteStreams(cancellationToken)) + { + yield return item; + } + } + + private async IAsyncEnumerable GetAllMovies([EnumeratorCancellation] CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCodes); + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + ConfiguredCancelableAsyncEnumerable movies = dbContext.Movies + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .AsAsyncEnumerable() + .WithCancellation(cancellationToken); + + await foreach (var movie in movies) + { + yield return movie; + } + } + + private async IAsyncEnumerable GetAllShows([EnumeratorCancellation] CancellationToken cancellationToken) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + ConfiguredCancelableAsyncEnumerable shows = dbContext.Shows + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .AsAsyncEnumerable() + .WithCancellation(cancellationToken); + + await foreach (var movie in shows) + { + yield return movie; + } } - public IAsyncEnumerable GetAllMediaItems() + private async IAsyncEnumerable GetAllSeasons([EnumeratorCancellation] CancellationToken cancellationToken) { - TvContext dbContext = _dbContextFactory.CreateDbContext(); - return dbContext.MediaItems + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + ConfiguredCancelableAsyncEnumerable seasons = dbContext.Seasons .AsNoTracking() - .Include(mi => mi.Collections) - .Include(mi => mi.LibraryPath) - .ThenInclude(lp => lp.Library) - .Include(mi => (mi as Movie).MovieMetadata) - .ThenInclude(mm => mm.Genres) - .Include(mi => (mi as Movie).MovieMetadata) - .ThenInclude(mm => mm.Tags) - .Include(mi => (mi as Movie).MovieMetadata) - .ThenInclude(mm => mm.Studios) - .Include(mi => (mi as Movie).MovieMetadata) - .ThenInclude(mm => mm.Actors) - .Include(mi => (mi as Movie).MovieMetadata) - .ThenInclude(mm => mm.Directors) - .Include(mi => (mi as Movie).MovieMetadata) - .ThenInclude(mm => mm.Writers) - .Include(mi => (mi as Movie).MovieMetadata) - .ThenInclude(mm => mm.Guids) - .Include(mi => (mi as Movie).MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(mi => (mi as Movie).MediaVersions) - .ThenInclude(mm => mm.Streams) - .Include(mi => (mi as Episode).EpisodeMetadata) - .ThenInclude(em => em.Genres) - .Include(mi => (mi as Episode).EpisodeMetadata) - .ThenInclude(em => em.Tags) - .Include(mi => (mi as Episode).EpisodeMetadata) - .ThenInclude(em => em.Studios) - .Include(mi => (mi as Episode).EpisodeMetadata) - .ThenInclude(em => em.Actors) - .Include(mi => (mi as Episode).EpisodeMetadata) - .ThenInclude(em => em.Directors) - .Include(mi => (mi as Episode).EpisodeMetadata) - .ThenInclude(em => em.Writers) - .Include(mi => (mi as Episode).EpisodeMetadata) - .ThenInclude(em => em.Guids) - .Include(mi => (mi as Episode).MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(mi => (mi as Episode).MediaVersions) - .ThenInclude(em => em.Streams) - .Include(mi => (mi as Episode).MediaVersions) - .ThenInclude(em => em.MediaFiles) - .Include(mi => (mi as Episode).Season) - .ThenInclude(s => s.Show) - .ThenInclude(s => s.ShowMetadata) - .ThenInclude(s => s.Genres) - .Include(mi => (mi as Episode).Season) - .ThenInclude(s => s.Show) - .ThenInclude(s => s.ShowMetadata) - .ThenInclude(s => s.Tags) - .Include(mi => (mi as Episode).Season) - .ThenInclude(s => s.Show) - .ThenInclude(s => s.ShowMetadata) - .ThenInclude(s => s.Studios) - .Include(mi => (mi as Season).SeasonMetadata) - .ThenInclude(sm => sm.Genres) - .Include(mi => (mi as Season).SeasonMetadata) - .ThenInclude(sm => sm.Tags) - .Include(mi => (mi as Season).SeasonMetadata) - .ThenInclude(sm => sm.Studios) - .Include(mi => (mi as Season).SeasonMetadata) - .ThenInclude(sm => sm.Actors) - .Include(mi => (mi as Season).SeasonMetadata) - .ThenInclude(sm => sm.Guids) - .Include(mi => (mi as Season).Show) - .ThenInclude(sm => sm.ShowMetadata) - .ThenInclude(sm => sm.Genres) - .Include(mi => (mi as Season).Show) - .ThenInclude(sm => sm.ShowMetadata) - .ThenInclude(sm => sm.Tags) - .Include(mi => (mi as Season).Show) - .ThenInclude(sm => sm.ShowMetadata) - .ThenInclude(sm => sm.Studios) - .Include(mi => (mi as Show).ShowMetadata) - .ThenInclude(mm => mm.Genres) - .Include(mi => (mi as Show).ShowMetadata) - .ThenInclude(mm => mm.Tags) - .Include(mi => (mi as Show).ShowMetadata) - .ThenInclude(mm => mm.Studios) - .Include(mi => (mi as Show).ShowMetadata) - .ThenInclude(mm => mm.Actors) - .Include(mi => (mi as Show).ShowMetadata) - .ThenInclude(mm => mm.Guids) - .Include(mi => (mi as MusicVideo).Artist) - .ThenInclude(mm => mm.ArtistMetadata) - .Include(mi => (mi as MusicVideo).MusicVideoMetadata) - .ThenInclude(mm => mm.Artists) - .Include(mi => (mi as MusicVideo).MusicVideoMetadata) - .ThenInclude(mm => mm.Genres) - .Include(mi => (mi as MusicVideo).MusicVideoMetadata) - .ThenInclude(mm => mm.Tags) - .Include(mi => (mi as MusicVideo).MusicVideoMetadata) - .ThenInclude(mm => mm.Studios) - .Include(mi => (mi as MusicVideo).MusicVideoMetadata) - .ThenInclude(mm => mm.Guids) - .Include(mi => (mi as MusicVideo).MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(mi => (mi as MusicVideo).MediaVersions) - .ThenInclude(mm => mm.Streams) - .Include(mi => (mi as Artist).ArtistMetadata) - .ThenInclude(mm => mm.Genres) - .Include(mi => (mi as Artist).ArtistMetadata) - .ThenInclude(mm => mm.Styles) - .Include(mi => (mi as Artist).ArtistMetadata) - .ThenInclude(mm => mm.Moods) - .Include(mi => (mi as Artist).ArtistMetadata) - .ThenInclude(mm => mm.Guids) - .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Genres) - .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Tags) - .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Studios) - .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Actors) - .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Directors) - .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Writers) - .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Guids) - .Include(mi => (mi as OtherVideo).MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(mi => (mi as OtherVideo).MediaVersions) - .ThenInclude(ovm => ovm.Streams) - .Include(mi => (mi as Song).SongMetadata) - .ThenInclude(mm => mm.Tags) - .Include(mi => (mi as Song).SongMetadata) - .ThenInclude(mm => mm.Genres) - .Include(mi => (mi as Song).SongMetadata) - .ThenInclude(mm => mm.Guids) - .Include(mi => (mi as Song).MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(mi => (mi as Song).MediaVersions) - .ThenInclude(mm => mm.Streams) - .Include(mi => (mi as Image).ImageMetadata) - .ThenInclude(mm => mm.Tags) - .Include(mi => (mi as Image).ImageMetadata) - .ThenInclude(mm => mm.Genres) - .Include(mi => (mi as Image).ImageMetadata) - .ThenInclude(mm => mm.Guids) - .Include(mi => (mi as Image).MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(mi => (mi as Image).MediaVersions) - .ThenInclude(mm => mm.Streams) - .Include(mi => (mi as Image).MediaVersions) - .ThenInclude(mm => mm.MediaFiles) - .Include(mi => (mi as RemoteStream).RemoteStreamMetadata) - .ThenInclude(mm => mm.Tags) - .Include(mi => (mi as RemoteStream).RemoteStreamMetadata) - .ThenInclude(mm => mm.Genres) - .Include(mi => (mi as RemoteStream).RemoteStreamMetadata) - .ThenInclude(mm => mm.Guids) - .Include(mi => (mi as RemoteStream).MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(mi => (mi as RemoteStream).MediaVersions) - .ThenInclude(mm => mm.Streams) - .Include(mi => (mi as RemoteStream).MediaVersions) - .ThenInclude(mm => mm.MediaFiles) - .Include(mi => mi.TraktListItems) - .ThenInclude(tli => tli.TraktList) - .AsAsyncEnumerable(); + .IncludeForSearch() + .AsSplitQuery() + .AsAsyncEnumerable() + .WithCancellation(cancellationToken); + + await foreach (var movie in seasons) + { + yield return movie; + } + } + + private async IAsyncEnumerable GetAllEpisodes([EnumeratorCancellation] CancellationToken cancellationToken) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + ConfiguredCancelableAsyncEnumerable episodes = dbContext.Episodes + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .AsAsyncEnumerable() + .WithCancellation(cancellationToken); + + await foreach (var movie in episodes) + { + yield return movie; + } + } + + private async IAsyncEnumerable GetAllMusicVideos( + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + ConfiguredCancelableAsyncEnumerable musicVideos = dbContext.MusicVideos + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .AsAsyncEnumerable() + .WithCancellation(cancellationToken); + + await foreach (var movie in musicVideos) + { + yield return movie; + } + } + + private async IAsyncEnumerable GetAllArtists([EnumeratorCancellation] CancellationToken cancellationToken) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + ConfiguredCancelableAsyncEnumerable artists = dbContext.Artists + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .AsAsyncEnumerable() + .WithCancellation(cancellationToken); + + await foreach (var movie in artists) + { + yield return movie; + } + } + + private async IAsyncEnumerable GetAllOtherVideos( + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + ConfiguredCancelableAsyncEnumerable otherVideos = dbContext.OtherVideos + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .AsAsyncEnumerable() + .WithCancellation(cancellationToken); + + await foreach (var movie in otherVideos) + { + yield return movie; + } + } + + private async IAsyncEnumerable GetAllSongs([EnumeratorCancellation] CancellationToken cancellationToken) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + ConfiguredCancelableAsyncEnumerable songs = dbContext.Songs + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .AsAsyncEnumerable() + .WithCancellation(cancellationToken); + + await foreach (var movie in songs) + { + yield return movie; + } + } + + private async IAsyncEnumerable GetAllImages([EnumeratorCancellation] CancellationToken cancellationToken) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + ConfiguredCancelableAsyncEnumerable images = dbContext.Images + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .AsAsyncEnumerable() + .WithCancellation(cancellationToken); + + await foreach (var movie in images) + { + yield return movie; + } + } + + private async IAsyncEnumerable GetAllRemoteStreams( + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + ConfiguredCancelableAsyncEnumerable remoteStreams = dbContext.RemoteStreams + .AsNoTracking() + .IncludeForSearch() + .AsSplitQuery() + .AsAsyncEnumerable() + .WithCancellation(cancellationToken); + + await foreach (var movie in remoteStreams) + { + yield return movie; + } } } diff --git a/ErsatzTV.Infrastructure/Extensions/LanguageCodeQueryableExtensions.cs b/ErsatzTV.Infrastructure/Extensions/LanguageCodeQueryableExtensions.cs index a9c66d63c..011f8f834 100644 --- a/ErsatzTV.Infrastructure/Extensions/LanguageCodeQueryableExtensions.cs +++ b/ErsatzTV.Infrastructure/Extensions/LanguageCodeQueryableExtensions.cs @@ -5,38 +5,6 @@ namespace ErsatzTV.Infrastructure.Extensions; public static class LanguageCodeQueryableExtensions { - public static async Task> GetAllLanguageCodes( - this IQueryable languageCodes, - string mediaCode) - { - if (string.IsNullOrWhiteSpace(mediaCode)) - { - return new List(); - } - - string code = mediaCode.ToLowerInvariant(); - - List maybeLanguages = await languageCodes - .Filter(lc => lc.ThreeCode1 == code || lc.ThreeCode2 == code) - .ToListAsync(); - - var result = new System.Collections.Generic.HashSet(); - foreach (LanguageCode language in maybeLanguages) - { - if (!string.IsNullOrWhiteSpace(language.ThreeCode1)) - { - result.Add(language.ThreeCode1); - } - - if (!string.IsNullOrWhiteSpace(language.ThreeCode2)) - { - result.Add(language.ThreeCode2); - } - } - - return result.ToList(); - } - public static async Task> GetAllLanguageCodes( this IQueryable languageCodes, List mediaCodes) diff --git a/ErsatzTV.Infrastructure/Extensions/QueryableExtensions.cs b/ErsatzTV.Infrastructure/Extensions/QueryableExtensions.cs index 8fcdd88b2..7f0b1feac 100644 --- a/ErsatzTV.Infrastructure/Extensions/QueryableExtensions.cs +++ b/ErsatzTV.Infrastructure/Extensions/QueryableExtensions.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using ErsatzTV.Core.Domain; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Infrastructure.Extensions; @@ -11,4 +12,135 @@ public static class QueryableExtensions Expression> predicate, CancellationToken cancellationToken) where T : class => await enumerable.OrderBy(keySelector).FirstOrDefaultAsync(predicate, cancellationToken); + + public static IQueryable IncludeForSearch(this IQueryable movies) => + movies + .Include(mi => mi.Collections) + .Include(mi => mi.LibraryPath).ThenInclude(lp => lp.Library) + .Include(mi => mi.TraktListItems).ThenInclude(tli => tli.TraktList) + .Include(m => m.MovieMetadata).ThenInclude(mm => mm.Genres) + .Include(m => m.MovieMetadata).ThenInclude(mm => mm.Tags) + .Include(m => m.MovieMetadata).ThenInclude(mm => mm.Studios) + .Include(m => m.MovieMetadata).ThenInclude(mm => mm.Actors) + .Include(m => m.MovieMetadata).ThenInclude(mm => mm.Directors) + .Include(m => m.MovieMetadata).ThenInclude(mm => mm.Writers) + .Include(m => m.MovieMetadata).ThenInclude(mm => mm.Guids) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.Chapters) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.Streams) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.MediaFiles); + + public static IQueryable IncludeForSearch(this IQueryable episodes) => + episodes + .Include(mi => mi.Collections) + .Include(mi => mi.LibraryPath).ThenInclude(lp => lp.Library) + .Include(mi => mi.TraktListItems).ThenInclude(tli => tli.TraktList) + .Include(m => m.EpisodeMetadata).ThenInclude(mm => mm.Genres) + .Include(m => m.EpisodeMetadata).ThenInclude(mm => mm.Tags) + .Include(m => m.EpisodeMetadata).ThenInclude(mm => mm.Studios) + .Include(m => m.EpisodeMetadata).ThenInclude(mm => mm.Actors) + .Include(m => m.EpisodeMetadata).ThenInclude(mm => mm.Directors) + .Include(m => m.EpisodeMetadata).ThenInclude(mm => mm.Writers) + .Include(m => m.EpisodeMetadata).ThenInclude(mm => mm.Guids) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.Chapters) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.Streams) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.MediaFiles) + .Include(m => m.Season).ThenInclude(s => s.Show).ThenInclude(s => s.ShowMetadata).ThenInclude(s => s.Genres) + .Include(m => m.Season).ThenInclude(s => s.Show).ThenInclude(s => s.ShowMetadata).ThenInclude(s => s.Tags) + .Include(m => m.Season).ThenInclude(s => s.Show).ThenInclude(s => s.ShowMetadata).ThenInclude(s => s.Studios); + + public static IQueryable IncludeForSearch(this IQueryable seasons) => + seasons + .Include(mi => mi.Collections) + .Include(mi => mi.LibraryPath).ThenInclude(lp => lp.Library) + .Include(mi => mi.TraktListItems).ThenInclude(tli => tli.TraktList) + .Include(m => m.SeasonMetadata).ThenInclude(mm => mm.Genres) + .Include(m => m.SeasonMetadata).ThenInclude(mm => mm.Tags) + .Include(m => m.SeasonMetadata).ThenInclude(mm => mm.Studios) + .Include(m => m.SeasonMetadata).ThenInclude(mm => mm.Actors) + .Include(m => m.SeasonMetadata).ThenInclude(mm => mm.Guids) + .Include(s => s.Show).ThenInclude(s => s.ShowMetadata).ThenInclude(s => s.Genres) + .Include(s => s.Show).ThenInclude(s => s.ShowMetadata).ThenInclude(s => s.Tags) + .Include(s => s.Show).ThenInclude(s => s.ShowMetadata).ThenInclude(s => s.Studios); + + public static IQueryable IncludeForSearch(this IQueryable shows) => + shows + .Include(mi => mi.Collections) + .Include(mi => mi.LibraryPath).ThenInclude(lp => lp.Library) + .Include(mi => mi.TraktListItems).ThenInclude(tli => tli.TraktList) + .Include(m => m.ShowMetadata).ThenInclude(mm => mm.Genres) + .Include(m => m.ShowMetadata).ThenInclude(mm => mm.Tags) + .Include(m => m.ShowMetadata).ThenInclude(mm => mm.Studios) + .Include(m => m.ShowMetadata).ThenInclude(mm => mm.Actors) + .Include(m => m.ShowMetadata).ThenInclude(mm => mm.Guids); + + public static IQueryable IncludeForSearch(this IQueryable musicVideos) => + musicVideos + .Include(mi => mi.Collections) + .Include(mi => mi.LibraryPath).ThenInclude(lp => lp.Library) + .Include(m => m.Artist).ThenInclude(a => a.ArtistMetadata) + .Include(m => m.MusicVideoMetadata).ThenInclude(mm => mm.Artists) + .Include(m => m.MusicVideoMetadata).ThenInclude(mm => mm.Genres) + .Include(m => m.MusicVideoMetadata).ThenInclude(mm => mm.Tags) + .Include(m => m.MusicVideoMetadata).ThenInclude(mm => mm.Studios) + .Include(m => m.MusicVideoMetadata).ThenInclude(mm => mm.Guids) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.Chapters) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.Streams) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.MediaFiles); + + public static IQueryable IncludeForSearch(this IQueryable artists) => + artists + .Include(mi => mi.Collections) + .Include(mi => mi.LibraryPath).ThenInclude(lp => lp.Library) + .Include(m => m.ArtistMetadata).ThenInclude(mm => mm.Genres) + .Include(m => m.ArtistMetadata).ThenInclude(mm => mm.Styles) + .Include(m => m.ArtistMetadata).ThenInclude(mm => mm.Moods) + .Include(m => m.ArtistMetadata).ThenInclude(mm => mm.Guids); + + public static IQueryable IncludeForSearch(this IQueryable otherVideos) => + otherVideos + .Include(mi => mi.Collections) + .Include(mi => mi.LibraryPath).ThenInclude(lp => lp.Library) + .Include(m => m.OtherVideoMetadata).ThenInclude(mm => mm.Genres) + .Include(m => m.OtherVideoMetadata).ThenInclude(mm => mm.Tags) + .Include(m => m.OtherVideoMetadata).ThenInclude(mm => mm.Studios) + .Include(m => m.OtherVideoMetadata).ThenInclude(mm => mm.Actors) + .Include(m => m.OtherVideoMetadata).ThenInclude(mm => mm.Directors) + .Include(m => m.OtherVideoMetadata).ThenInclude(mm => mm.Writers) + .Include(m => m.OtherVideoMetadata).ThenInclude(mm => mm.Guids) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.Chapters) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.Streams) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.MediaFiles); + + public static IQueryable IncludeForSearch(this IQueryable songs) => + songs + .Include(mi => mi.Collections) + .Include(mi => mi.LibraryPath).ThenInclude(lp => lp.Library) + .Include(m => m.SongMetadata).ThenInclude(mm => mm.Tags) + .Include(m => m.SongMetadata).ThenInclude(mm => mm.Genres) + .Include(m => m.SongMetadata).ThenInclude(mm => mm.Guids) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.Chapters) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.Streams) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.MediaFiles); + + public static IQueryable IncludeForSearch(this IQueryable images) => + images + .Include(mi => mi.Collections) + .Include(mi => mi.LibraryPath).ThenInclude(lp => lp.Library) + .Include(m => m.ImageMetadata).ThenInclude(mm => mm.Tags) + .Include(m => m.ImageMetadata).ThenInclude(mm => mm.Genres) + .Include(m => m.ImageMetadata).ThenInclude(mm => mm.Guids) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.Chapters) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.Streams) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.MediaFiles); + + public static IQueryable IncludeForSearch(this IQueryable images) => + images + .Include(mi => mi.Collections) + .Include(mi => mi.LibraryPath).ThenInclude(lp => lp.Library) + .Include(m => m.RemoteStreamMetadata).ThenInclude(mm => mm.Tags) + .Include(m => m.RemoteStreamMetadata).ThenInclude(mm => mm.Genres) + .Include(m => m.RemoteStreamMetadata).ThenInclude(mm => mm.Guids) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.Chapters) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.Streams) + .Include(m => m.MediaVersions).ThenInclude(mv => mv.MediaFiles); } diff --git a/ErsatzTV.Infrastructure/Metadata/LanguageCodeCache.cs b/ErsatzTV.Infrastructure/Metadata/LanguageCodeCache.cs new file mode 100644 index 000000000..0d80a6355 --- /dev/null +++ b/ErsatzTV.Infrastructure/Metadata/LanguageCodeCache.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using ErsatzTV.Core.Interfaces.Metadata; + +namespace ErsatzTV.Infrastructure.Metadata; + +public class LanguageCodeCache : ILanguageCodeCache +{ + public IReadOnlyDictionary CodeToGroupLookup { get; private set; } + + public IReadOnlyList AllGroups { get; private set; } + + public async Task Load(CancellationToken cancellationToken) + { + var lookup = new Dictionary(); + var allGroups = new List(); + + var assembly = Assembly.GetEntryAssembly(); + if (assembly != null) + { + await using Stream resource = assembly.GetManifestResourceStream("ErsatzTV.Resources.ISO-639-2_utf-8.txt"); + if (resource != null) + { + using var reader = new StreamReader(resource); + while (!reader.EndOfStream) + { + string line = await reader.ReadLineAsync(cancellationToken); + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + string[] split = line.Split("|"); + if (split.Length != 5) + { + continue; + } + + string[] group = new[] { split[0], split[1] } + .Where(c => !string.IsNullOrWhiteSpace(c)) + .Distinct() + .ToArray(); + + if (group.Length > 0) + { + allGroups.Add(group); + + foreach (string code in group) + { + lookup[code] = group; + } + } + } + } + } + + CodeToGroupLookup = lookup; + AllGroups = allGroups; + } +} diff --git a/ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs b/ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs index c63ed0083..0de9d7070 100644 --- a/ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs +++ b/ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs @@ -6,7 +6,6 @@ using Elastic.Clients.Elasticsearch.IndexManagement; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Repositories.Caching; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; using ErsatzTV.FFmpeg; @@ -69,8 +68,9 @@ public class ElasticSearchIndex : ISearchIndex } public async Task Rebuild( - ICachingSearchRepository searchRepository, + ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, CancellationToken cancellationToken) { DeleteIndexResponse deleteResponse = await _client.Indices.DeleteAsync(IndexName, cancellationToken); @@ -85,17 +85,18 @@ public class ElasticSearchIndex : ISearchIndex return Unit.Default; } - await foreach (MediaItem mediaItem in searchRepository.GetAllMediaItems().WithCancellation(cancellationToken)) + await foreach (MediaItem mediaItem in searchRepository.GetAllMediaItems(cancellationToken)) { - await RebuildItem(searchRepository, fallbackMetadataProvider, mediaItem); + await RebuildItem(searchRepository, fallbackMetadataProvider, languageCodeService, mediaItem); } return Unit.Default; } public async Task RebuildItems( - ICachingSearchRepository searchRepository, + ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, IEnumerable itemIds, CancellationToken cancellationToken) { @@ -103,7 +104,7 @@ public class ElasticSearchIndex : ISearchIndex { foreach (MediaItem mediaItem in await searchRepository.GetItemToIndex(id, cancellationToken)) { - await RebuildItem(searchRepository, fallbackMetadataProvider, mediaItem); + await RebuildItem(searchRepository, fallbackMetadataProvider, languageCodeService, mediaItem); } } @@ -111,8 +112,9 @@ public class ElasticSearchIndex : ISearchIndex } public async Task UpdateItems( - ICachingSearchRepository searchRepository, + ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, List items) { foreach (MediaItem item in items) @@ -120,31 +122,31 @@ public class ElasticSearchIndex : ISearchIndex switch (item) { case Movie movie: - await UpdateMovie(searchRepository, movie); + await UpdateMovie(languageCodeService, movie); break; case Show show: - await UpdateShow(searchRepository, show); + await UpdateShow(searchRepository, languageCodeService, show); break; case Season season: - await UpdateSeason(searchRepository, season); + await UpdateSeason(searchRepository, languageCodeService, season); break; case Artist artist: - await UpdateArtist(searchRepository, artist); + await UpdateArtist(searchRepository, languageCodeService, artist); break; case MusicVideo musicVideo: - await UpdateMusicVideo(searchRepository, musicVideo); + await UpdateMusicVideo(languageCodeService, musicVideo); break; case Episode episode: - await UpdateEpisode(searchRepository, fallbackMetadataProvider, episode); + await UpdateEpisode(languageCodeService, fallbackMetadataProvider, episode); break; case OtherVideo otherVideo: - await UpdateOtherVideo(searchRepository, otherVideo); + await UpdateOtherVideo(languageCodeService, otherVideo); break; case Song song: - await UpdateSong(searchRepository, song); + await UpdateSong(languageCodeService, song); break; case Image image: - await UpdateImage(searchRepository, image); + await UpdateImage(languageCodeService, image); break; } } @@ -266,38 +268,39 @@ public class ElasticSearchIndex : ISearchIndex private async Task RebuildItem( ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, MediaItem mediaItem) { switch (mediaItem) { case Movie movie: - await UpdateMovie(searchRepository, movie); + await UpdateMovie(languageCodeService, movie); break; case Show show: - await UpdateShow(searchRepository, show); + await UpdateShow(searchRepository, languageCodeService, show); break; case Season season: - await UpdateSeason(searchRepository, season); + await UpdateSeason(searchRepository, languageCodeService, season); break; case Artist artist: - await UpdateArtist(searchRepository, artist); + await UpdateArtist(searchRepository, languageCodeService, artist); break; case MusicVideo musicVideo: - await UpdateMusicVideo(searchRepository, musicVideo); + await UpdateMusicVideo(languageCodeService, musicVideo); break; case Episode episode: - await UpdateEpisode(searchRepository, fallbackMetadataProvider, episode); + await UpdateEpisode(languageCodeService, fallbackMetadataProvider, episode); break; case OtherVideo otherVideo: - await UpdateOtherVideo(searchRepository, otherVideo); + await UpdateOtherVideo(languageCodeService, otherVideo); break; case Song song: - await UpdateSong(searchRepository, song); + await UpdateSong(languageCodeService, song); break; } } - private async Task UpdateMovie(ISearchRepository searchRepository, Movie movie) + private async Task UpdateMovie(ILanguageCodeService languageCodeService, Movie movie) { foreach (MovieMetadata metadata in movie.MovieMetadata.HeadOrNone()) { @@ -315,9 +318,9 @@ public class ElasticSearchIndex : ISearchIndex JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), State = movie.State.ToString(), MetadataKind = metadata.MetadataKind.ToString(), - Language = await GetLanguages(searchRepository, movie.MediaVersions), + Language = GetLanguages(languageCodeService, movie.MediaVersions), LanguageTag = GetLanguageTags(movie.MediaVersions), - SubLanguage = await GetSubLanguages(searchRepository, movie.MediaVersions), + SubLanguage = GetSubLanguages(languageCodeService, movie.MediaVersions), SubLanguageTag = GetSubLanguageTags(movie.MediaVersions), ContentRating = GetContentRatings(metadata.ContentRating), ReleaseDate = GetReleaseDate(metadata.ReleaseDate), @@ -356,7 +359,7 @@ public class ElasticSearchIndex : ISearchIndex } } - private async Task UpdateShow(ISearchRepository searchRepository, Show show) + private async Task UpdateShow(ISearchRepository searchRepository, ILanguageCodeService languageCodeService, Show show) { foreach (ShowMetadata metadata in show.ShowMetadata.HeadOrNone()) { @@ -374,10 +377,10 @@ public class ElasticSearchIndex : ISearchIndex JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), State = show.State.ToString(), MetadataKind = metadata.MetadataKind.ToString(), - Language = await GetLanguages(searchRepository, await searchRepository.GetLanguagesForShow(show)), + Language = GetLanguages(languageCodeService, await searchRepository.GetLanguagesForShow(show)), LanguageTag = await searchRepository.GetLanguagesForShow(show), - SubLanguage = await GetLanguages( - searchRepository, + SubLanguage = GetLanguages( + languageCodeService, await searchRepository.GetSubLanguagesForShow(show)), SubLanguageTag = await searchRepository.GetSubLanguagesForShow(show), ContentRating = GetContentRatings(metadata.ContentRating), @@ -412,7 +415,10 @@ public class ElasticSearchIndex : ISearchIndex } } - private async Task UpdateSeason(ISearchRepository searchRepository, Season season) + private async Task UpdateSeason( + ISearchRepository searchRepository, + ILanguageCodeService languageCodeService, + Season season) { foreach (SeasonMetadata metadata in season.SeasonMetadata.HeadOrNone()) foreach (ShowMetadata showMetadata in season.Show.ShowMetadata.HeadOrNone()) @@ -442,12 +448,12 @@ public class ElasticSearchIndex : ISearchIndex ShowTag = showMetadata.Tags.Map(t => t.Name).ToList(), ShowStudio = showMetadata.Studios.Map(s => s.Name).ToList(), ShowContentRating = GetContentRatings(showMetadata.ContentRating), - Language = await GetLanguages( - searchRepository, + Language = GetLanguages( + languageCodeService, await searchRepository.GetLanguagesForSeason(season)), LanguageTag = await searchRepository.GetLanguagesForSeason(season), - SubLanguage = await GetLanguages( - searchRepository, + SubLanguage = GetLanguages( + languageCodeService, await searchRepository.GetSubLanguagesForSeason(season)), SubLanguageTag = await searchRepository.GetSubLanguagesForSeason(season), ContentRating = GetContentRatings(showMetadata.ContentRating), @@ -474,7 +480,10 @@ public class ElasticSearchIndex : ISearchIndex } } - private async Task UpdateArtist(ISearchRepository searchRepository, Artist artist) + private async Task UpdateArtist( + ISearchRepository searchRepository, + ILanguageCodeService languageCodeService, + Artist artist) { foreach (ArtistMetadata metadata in artist.ArtistMetadata.HeadOrNone()) { @@ -492,12 +501,12 @@ public class ElasticSearchIndex : ISearchIndex JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), State = artist.State.ToString(), MetadataKind = metadata.MetadataKind.ToString(), - Language = await GetLanguages( - searchRepository, + Language = GetLanguages( + languageCodeService, await searchRepository.GetLanguagesForArtist(artist)), LanguageTag = await searchRepository.GetLanguagesForArtist(artist), - SubLanguage = await GetLanguages( - searchRepository, + SubLanguage = GetLanguages( + languageCodeService, await searchRepository.GetSubLanguagesForArtist(artist)), SubLanguageTag = await searchRepository.GetSubLanguagesForArtist(artist), AddedDate = GetAddedDate(metadata.DateAdded), @@ -521,7 +530,7 @@ public class ElasticSearchIndex : ISearchIndex } } - private async Task UpdateMusicVideo(ISearchRepository searchRepository, MusicVideo musicVideo) + private async Task UpdateMusicVideo(ILanguageCodeService languageCodeService, MusicVideo musicVideo) { foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata.HeadOrNone()) { @@ -539,9 +548,9 @@ public class ElasticSearchIndex : ISearchIndex JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), State = musicVideo.State.ToString(), MetadataKind = metadata.MetadataKind.ToString(), - Language = await GetLanguages(searchRepository, musicVideo.MediaVersions), + Language = GetLanguages(languageCodeService, musicVideo.MediaVersions), LanguageTag = GetLanguageTags(musicVideo.MediaVersions), - SubLanguage = await GetSubLanguages(searchRepository, musicVideo.MediaVersions), + SubLanguage = GetSubLanguages(languageCodeService, musicVideo.MediaVersions), SubLanguageTag = GetSubLanguageTags(musicVideo.MediaVersions), ReleaseDate = GetReleaseDate(metadata.ReleaseDate), AddedDate = GetAddedDate(metadata.DateAdded), @@ -589,7 +598,7 @@ public class ElasticSearchIndex : ISearchIndex } private async Task UpdateEpisode( - ISearchRepository searchRepository, + ILanguageCodeService languageCodeService, IFallbackMetadataProvider fallbackMetadataProvider, Episode episode) { @@ -622,9 +631,9 @@ public class ElasticSearchIndex : ISearchIndex MetadataKind = metadata.MetadataKind.ToString(), SeasonNumber = episode.Season?.SeasonNumber ?? 0, EpisodeNumber = metadata.EpisodeNumber, - Language = await GetLanguages(searchRepository, episode.MediaVersions), + Language = GetLanguages(languageCodeService, episode.MediaVersions), LanguageTag = GetLanguageTags(episode.MediaVersions), - SubLanguage = await GetSubLanguages(searchRepository, episode.MediaVersions), + SubLanguage = GetSubLanguages(languageCodeService, episode.MediaVersions), SubLanguageTag = GetSubLanguageTags(episode.MediaVersions), ReleaseDate = GetReleaseDate(metadata.ReleaseDate), AddedDate = GetAddedDate(metadata.DateAdded), @@ -671,7 +680,7 @@ public class ElasticSearchIndex : ISearchIndex } } - private async Task UpdateOtherVideo(ISearchRepository searchRepository, OtherVideo otherVideo) + private async Task UpdateOtherVideo(ILanguageCodeService languageCodeService, OtherVideo otherVideo) { foreach (OtherVideoMetadata metadata in otherVideo.OtherVideoMetadata.HeadOrNone()) { @@ -689,9 +698,9 @@ public class ElasticSearchIndex : ISearchIndex JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), State = otherVideo.State.ToString(), MetadataKind = metadata.MetadataKind.ToString(), - Language = await GetLanguages(searchRepository, otherVideo.MediaVersions), + Language = GetLanguages(languageCodeService, otherVideo.MediaVersions), LanguageTag = GetLanguageTags(otherVideo.MediaVersions), - SubLanguage = await GetSubLanguages(searchRepository, otherVideo.MediaVersions), + SubLanguage = GetSubLanguages(languageCodeService, otherVideo.MediaVersions), SubLanguageTag = GetSubLanguageTags(otherVideo.MediaVersions), ContentRating = GetContentRatings(metadata.ContentRating), ReleaseDate = GetReleaseDate(metadata.ReleaseDate), @@ -724,7 +733,7 @@ public class ElasticSearchIndex : ISearchIndex } } - private async Task UpdateSong(ISearchRepository searchRepository, Song song) + private async Task UpdateSong(ILanguageCodeService languageCodeService, Song song) { foreach (SongMetadata metadata in song.SongMetadata.HeadOrNone()) { @@ -745,9 +754,9 @@ public class ElasticSearchIndex : ISearchIndex JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), State = song.State.ToString(), MetadataKind = metadata.MetadataKind.ToString(), - Language = await GetLanguages(searchRepository, song.MediaVersions), + Language = GetLanguages(languageCodeService, song.MediaVersions), LanguageTag = GetLanguageTags(song.MediaVersions), - SubLanguage = await GetSubLanguages(searchRepository, song.MediaVersions), + SubLanguage = GetSubLanguages(languageCodeService, song.MediaVersions), SubLanguageTag = GetSubLanguageTags(song.MediaVersions), AddedDate = GetAddedDate(metadata.DateAdded), Album = metadata.Album ?? string.Empty, @@ -776,7 +785,7 @@ public class ElasticSearchIndex : ISearchIndex } } - private async Task UpdateImage(ISearchRepository searchRepository, Image image) + private async Task UpdateImage(ILanguageCodeService languageCodeService, Image image) { foreach (ImageMetadata metadata in image.ImageMetadata.HeadOrNone()) { @@ -794,9 +803,9 @@ public class ElasticSearchIndex : ISearchIndex JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), State = image.State.ToString(), MetadataKind = metadata.MetadataKind.ToString(), - Language = await GetLanguages(searchRepository, image.MediaVersions), + Language = GetLanguages(languageCodeService, image.MediaVersions), LanguageTag = GetLanguageTags(image.MediaVersions), - SubLanguage = await GetSubLanguages(searchRepository, image.MediaVersions), + SubLanguage = GetSubLanguages(languageCodeService, image.MediaVersions), SubLanguageTag = GetSubLanguageTags(image.MediaVersions), AddedDate = GetAddedDate(metadata.DateAdded), Genre = metadata.Genres.Map(g => g.Name).ToList(), @@ -853,9 +862,7 @@ public class ElasticSearchIndex : ISearchIndex return contentRatings; } - private async Task> GetLanguages( - ISearchRepository searchRepository, - IEnumerable mediaVersions) + private List GetLanguages(ILanguageCodeService languageCodeService, IEnumerable mediaVersions) { var result = new List(); @@ -867,14 +874,14 @@ public class ElasticSearchIndex : ISearchIndex .Distinct() .ToList(); - result.AddRange(await GetLanguages(searchRepository, mediaCodes)); + result.AddRange(GetLanguages(languageCodeService, mediaCodes)); } return result; } - private async Task> GetSubLanguages( - ISearchRepository searchRepository, + private List GetSubLanguages( + ILanguageCodeService languageCodeService, IEnumerable mediaVersions) { var result = new List(); @@ -887,16 +894,16 @@ public class ElasticSearchIndex : ISearchIndex .Distinct() .ToList(); - result.AddRange(await GetLanguages(searchRepository, mediaCodes)); + result.AddRange(GetLanguages(languageCodeService, mediaCodes)); } return result; } - private async Task> GetLanguages(ISearchRepository searchRepository, List mediaCodes) + private List GetLanguages(ILanguageCodeService languageCodeService, List mediaCodes) { var englishNames = new System.Collections.Generic.HashSet(); - foreach (string code in await searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes)) + foreach (string code in languageCodeService.GetAllLanguageCodes(mediaCodes)) { Option maybeCultureInfo = _cultureInfos.Find(ci => string.Equals( ci.ThreeLetterISOLanguageName, diff --git a/ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs b/ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs index 6ef148c46..5ec9e950f 100644 --- a/ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs +++ b/ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs @@ -4,7 +4,6 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Repositories.Caching; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; using ErsatzTV.FFmpeg; @@ -155,8 +154,9 @@ public sealed class LuceneSearchIndex : ISearchIndex } public async Task UpdateItems( - ICachingSearchRepository searchRepository, + ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, List items) { foreach (MediaItem item in items) @@ -164,34 +164,34 @@ public sealed class LuceneSearchIndex : ISearchIndex switch (item) { case Movie movie: - await UpdateMovie(searchRepository, movie); + UpdateMovie(languageCodeService, movie); break; case Show show: - await UpdateShow(searchRepository, show); + await UpdateShow(searchRepository, languageCodeService, show); break; case Season season: - await UpdateSeason(searchRepository, season); + await UpdateSeason(searchRepository, languageCodeService, season); break; case Artist artist: - await UpdateArtist(searchRepository, artist); + await UpdateArtist(searchRepository, languageCodeService, artist); break; case MusicVideo musicVideo: - await UpdateMusicVideo(searchRepository, musicVideo); + UpdateMusicVideo(languageCodeService, musicVideo); break; case Episode episode: - await UpdateEpisode(searchRepository, fallbackMetadataProvider, episode); + UpdateEpisode(languageCodeService, fallbackMetadataProvider, episode); break; case OtherVideo otherVideo: - await UpdateOtherVideo(searchRepository, otherVideo); + UpdateOtherVideo(languageCodeService, otherVideo); break; case Song song: - await UpdateSong(searchRepository, song); + UpdateSong(languageCodeService, song); break; case Image image: - await UpdateImage(searchRepository, image); + UpdateImage(languageCodeService, image); break; case RemoteStream remoteStream: - await UpdateRemoteStream(searchRepository, remoteStream); + UpdateRemoteStream(languageCodeService, remoteStream); break; } } @@ -275,16 +275,17 @@ public sealed class LuceneSearchIndex : ISearchIndex } public async Task Rebuild( - ICachingSearchRepository searchRepository, + ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, CancellationToken cancellationToken) { _writer.DeleteAll(); _writer.Commit(); - await foreach (MediaItem mediaItem in searchRepository.GetAllMediaItems().WithCancellation(cancellationToken)) + await foreach (MediaItem mediaItem in searchRepository.GetAllMediaItems(cancellationToken)) { - await RebuildItem(searchRepository, fallbackMetadataProvider, mediaItem); + await RebuildItem(searchRepository, fallbackMetadataProvider, languageCodeService, mediaItem); } _writer.Commit(); @@ -292,8 +293,9 @@ public sealed class LuceneSearchIndex : ISearchIndex } public async Task RebuildItems( - ICachingSearchRepository searchRepository, + ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, IEnumerable itemIds, CancellationToken cancellationToken) { @@ -301,7 +303,7 @@ public sealed class LuceneSearchIndex : ISearchIndex { foreach (MediaItem mediaItem in await searchRepository.GetItemToIndex(id, cancellationToken)) { - await RebuildItem(searchRepository, fallbackMetadataProvider, mediaItem); + await RebuildItem(searchRepository, fallbackMetadataProvider, languageCodeService, mediaItem); } } @@ -337,39 +339,40 @@ public sealed class LuceneSearchIndex : ISearchIndex private async Task RebuildItem( ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider, + ILanguageCodeService languageCodeService, MediaItem mediaItem) { switch (mediaItem) { case Movie movie: - await UpdateMovie(searchRepository, movie); + UpdateMovie(languageCodeService, movie); break; case Show show: - await UpdateShow(searchRepository, show); + await UpdateShow(searchRepository, languageCodeService, show); break; case Season season: - await UpdateSeason(searchRepository, season); + await UpdateSeason(searchRepository, languageCodeService, season); break; case Artist artist: - await UpdateArtist(searchRepository, artist); + await UpdateArtist(searchRepository, languageCodeService, artist); break; case MusicVideo musicVideo: - await UpdateMusicVideo(searchRepository, musicVideo); + UpdateMusicVideo(languageCodeService, musicVideo); break; case Episode episode: - await UpdateEpisode(searchRepository, fallbackMetadataProvider, episode); + UpdateEpisode(languageCodeService, fallbackMetadataProvider, episode); break; case OtherVideo otherVideo: - await UpdateOtherVideo(searchRepository, otherVideo); + UpdateOtherVideo(languageCodeService, otherVideo); break; case Song song: - await UpdateSong(searchRepository, song); + UpdateSong(languageCodeService, song); break; case Image image: - await UpdateImage(searchRepository, image); + UpdateImage(languageCodeService, image); break; case RemoteStream remoteStream: - await UpdateRemoteStream(searchRepository, remoteStream); + UpdateRemoteStream(languageCodeService, remoteStream); break; } } @@ -421,7 +424,7 @@ public sealed class LuceneSearchIndex : ISearchIndex return new SearchPageMap(map); } - private async Task UpdateMovie(ISearchRepository searchRepository, Movie movie) + private void UpdateMovie(ILanguageCodeService languageCodeService, Movie movie) { Option maybeMetadata = movie.MovieMetadata.HeadOrNone(); if (maybeMetadata.IsSome) @@ -447,7 +450,7 @@ public sealed class LuceneSearchIndex : ISearchIndex new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) }; - await AddLanguages(searchRepository, doc, movie.MediaVersions); + AddLanguages(languageCodeService, doc, movie.MediaVersions); AddStatistics(doc, movie.MediaVersions); AddCollections(doc, movie.Collections); @@ -540,8 +543,8 @@ public sealed class LuceneSearchIndex : ISearchIndex } } - private async Task AddLanguages( - ISearchRepository searchRepository, + private void AddLanguages( + ILanguageCodeService languageCodeService, Document doc, ICollection mediaVersions) { @@ -552,7 +555,7 @@ public sealed class LuceneSearchIndex : ISearchIndex .Distinct() .ToList(); - await AddLanguages(searchRepository, doc, mediaCodes); + AddLanguages(languageCodeService, doc, mediaCodes); var subMediaCodes = mediaVersions .Map(mv => mv.Streams @@ -563,10 +566,10 @@ public sealed class LuceneSearchIndex : ISearchIndex .Distinct() .ToList(); - await AddSubLanguages(searchRepository, doc, subMediaCodes); + AddSubLanguages(languageCodeService, doc, subMediaCodes); } - private async Task AddLanguages(ISearchRepository searchRepository, Document doc, List mediaCodes) + private void AddLanguages(ILanguageCodeService languageCodeService, Document doc, List mediaCodes) { foreach (string code in mediaCodes.Where(c => !string.IsNullOrWhiteSpace(c)).Distinct()) { @@ -574,7 +577,7 @@ public sealed class LuceneSearchIndex : ISearchIndex } var englishNames = new System.Collections.Generic.HashSet(); - foreach (string code in await searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes)) + foreach (string code in languageCodeService.GetAllLanguageCodes(mediaCodes)) { Option maybeCultureInfo = _cultureInfos.Find(ci => string.Equals( ci.ThreeLetterISOLanguageName, @@ -592,7 +595,7 @@ public sealed class LuceneSearchIndex : ISearchIndex } } - private async Task AddSubLanguages(ISearchRepository searchRepository, Document doc, List mediaCodes) + private void AddSubLanguages(ILanguageCodeService languageCodeService, Document doc, List mediaCodes) { foreach (string code in mediaCodes.Where(c => !string.IsNullOrWhiteSpace(c)).Distinct()) { @@ -600,7 +603,7 @@ public sealed class LuceneSearchIndex : ISearchIndex } var englishNames = new System.Collections.Generic.HashSet(); - foreach (string code in await searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes)) + foreach (string code in languageCodeService.GetAllLanguageCodes(mediaCodes)) { Option maybeCultureInfo = _cultureInfos.Find(ci => string.Equals( ci.ThreeLetterISOLanguageName, @@ -618,7 +621,7 @@ public sealed class LuceneSearchIndex : ISearchIndex } } - private async Task UpdateShow(ISearchRepository searchRepository, Show show) + private async Task UpdateShow(ISearchRepository searchRepository, ILanguageCodeService languageCodeService, Show show) { Option maybeMetadata = show.ShowMetadata.HeadOrNone(); if (maybeMetadata.IsSome) @@ -645,10 +648,10 @@ public sealed class LuceneSearchIndex : ISearchIndex }; List languages = await searchRepository.GetLanguagesForShow(show); - await AddLanguages(searchRepository, doc, languages); + AddLanguages(languageCodeService, doc, languages); List subLanguages = await searchRepository.GetSubLanguagesForShow(show); - await AddSubLanguages(searchRepository, doc, subLanguages); + AddSubLanguages(languageCodeService, doc, subLanguages); AddCollections(doc, show.Collections); @@ -730,7 +733,10 @@ public sealed class LuceneSearchIndex : ISearchIndex } } - private async Task UpdateSeason(ISearchRepository searchRepository, Season season) + private async Task UpdateSeason( + ISearchRepository searchRepository, + ILanguageCodeService languageCodeService, + Season season) { Option maybeMetadata = season.SeasonMetadata.HeadOrNone(); Option maybeShowMetadata = season.Show.ShowMetadata.HeadOrNone(); @@ -791,10 +797,10 @@ public sealed class LuceneSearchIndex : ISearchIndex } List languages = await searchRepository.GetLanguagesForSeason(season); - await AddLanguages(searchRepository, doc, languages); + AddLanguages(languageCodeService, doc, languages); List subLanguages = await searchRepository.GetSubLanguagesForSeason(season); - await AddSubLanguages(searchRepository, doc, subLanguages); + AddSubLanguages(languageCodeService, doc, subLanguages); AddCollections(doc, season.Collections); @@ -849,7 +855,10 @@ public sealed class LuceneSearchIndex : ISearchIndex } } - private async Task UpdateArtist(ISearchRepository searchRepository, Artist artist) + private async Task UpdateArtist( + ISearchRepository searchRepository, + ILanguageCodeService languageCodeService, + Artist artist) { Option maybeMetadata = artist.ArtistMetadata.HeadOrNone(); if (maybeMetadata.IsSome) @@ -875,10 +884,10 @@ public sealed class LuceneSearchIndex : ISearchIndex }; List languages = await searchRepository.GetLanguagesForArtist(artist); - await AddLanguages(searchRepository, doc, languages); + AddLanguages(languageCodeService, doc, languages); List subLanguages = await searchRepository.GetSubLanguagesForArtist(artist); - await AddSubLanguages(searchRepository, doc, subLanguages); + AddSubLanguages(languageCodeService, doc, subLanguages); AddCollections(doc, artist.Collections); @@ -915,7 +924,7 @@ public sealed class LuceneSearchIndex : ISearchIndex } } - private async Task UpdateMusicVideo(ISearchRepository searchRepository, MusicVideo musicVideo) + private void UpdateMusicVideo(ILanguageCodeService languageCodeService, MusicVideo musicVideo) { Option maybeMetadata = musicVideo.MusicVideoMetadata.HeadOrNone(); if (maybeMetadata.IsSome) @@ -944,7 +953,7 @@ public sealed class LuceneSearchIndex : ISearchIndex new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) }; - await AddLanguages(searchRepository, doc, musicVideo.MediaVersions); + AddLanguages(languageCodeService, doc, musicVideo.MediaVersions); AddStatistics(doc, musicVideo.MediaVersions); AddCollections(doc, musicVideo.Collections); @@ -1022,8 +1031,8 @@ public sealed class LuceneSearchIndex : ISearchIndex } } - private async Task UpdateEpisode( - ISearchRepository searchRepository, + private void UpdateEpisode( + ILanguageCodeService languageCodeService, IFallbackMetadataProvider fallbackMetadataProvider, Episode episode) { @@ -1106,7 +1115,7 @@ public sealed class LuceneSearchIndex : ISearchIndex doc.Add(new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO)); } - await AddLanguages(searchRepository, doc, episode.MediaVersions); + AddLanguages(languageCodeService, doc, episode.MediaVersions); AddStatistics(doc, episode.MediaVersions); AddCollections(doc, episode.Collections); @@ -1183,7 +1192,7 @@ public sealed class LuceneSearchIndex : ISearchIndex } } - private async Task UpdateOtherVideo(ISearchRepository searchRepository, OtherVideo otherVideo) + private void UpdateOtherVideo(ILanguageCodeService languageCodeService, OtherVideo otherVideo) { Option maybeMetadata = otherVideo.OtherVideoMetadata.HeadOrNone(); if (maybeMetadata.IsSome) @@ -1209,7 +1218,7 @@ public sealed class LuceneSearchIndex : ISearchIndex new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) }; - await AddLanguages(searchRepository, doc, otherVideo.MediaVersions); + AddLanguages(languageCodeService, doc, otherVideo.MediaVersions); AddStatistics(doc, otherVideo.MediaVersions); AddCollections(doc, otherVideo.Collections); @@ -1286,7 +1295,7 @@ public sealed class LuceneSearchIndex : ISearchIndex } } - private async Task UpdateSong(ISearchRepository searchRepository, Song song) + private void UpdateSong(ILanguageCodeService languageCodeService, Song song) { Option maybeMetadata = song.SongMetadata.HeadOrNone(); foreach (var metadata in maybeMetadata) @@ -1313,7 +1322,7 @@ public sealed class LuceneSearchIndex : ISearchIndex new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) }; - await AddLanguages(searchRepository, doc, song.MediaVersions); + AddLanguages(languageCodeService, doc, song.MediaVersions); AddStatistics(doc, song.MediaVersions); AddCollections(doc, song.Collections); @@ -1362,7 +1371,7 @@ public sealed class LuceneSearchIndex : ISearchIndex } } - private async Task UpdateImage(ISearchRepository searchRepository, Image image) + private void UpdateImage(ILanguageCodeService languageCodeService, Image image) { Option maybeMetadata = image.ImageMetadata.HeadOrNone(); if (maybeMetadata.IsSome) @@ -1401,7 +1410,7 @@ public sealed class LuceneSearchIndex : ISearchIndex Field.Store.NO)); } - await AddLanguages(searchRepository, doc, image.MediaVersions); + AddLanguages(languageCodeService, doc, image.MediaVersions); AddStatistics(doc, image.MediaVersions); AddCollections(doc, image.Collections); @@ -1435,7 +1444,7 @@ public sealed class LuceneSearchIndex : ISearchIndex } } - private async Task UpdateRemoteStream(ISearchRepository searchRepository, RemoteStream remoteStream) + private void UpdateRemoteStream(ILanguageCodeService languageCodeService, RemoteStream remoteStream) { Option maybeMetadata = remoteStream.RemoteStreamMetadata.HeadOrNone(); if (maybeMetadata.IsSome) @@ -1474,7 +1483,7 @@ public sealed class LuceneSearchIndex : ISearchIndex Field.Store.NO)); } - await AddLanguages(searchRepository, doc, remoteStream.MediaVersions); + AddLanguages(languageCodeService, doc, remoteStream.MediaVersions); AddStatistics(doc, remoteStream.MediaVersions); AddCollections(doc, remoteStream.Collections); diff --git a/ErsatzTV.Scanner/Program.cs b/ErsatzTV.Scanner/Program.cs index 500c2abd7..febbb37c1 100644 --- a/ErsatzTV.Scanner/Program.cs +++ b/ErsatzTV.Scanner/Program.cs @@ -11,7 +11,6 @@ using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Repositories.Caching; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Jellyfin; using ErsatzTV.Core.Metadata; @@ -21,7 +20,6 @@ using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data.Repositories; -using ErsatzTV.Infrastructure.Data.Repositories.Caching; using ErsatzTV.Infrastructure.Emby; using ErsatzTV.Infrastructure.Images; using ErsatzTV.Infrastructure.Jellyfin; @@ -185,7 +183,7 @@ public class Program services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -252,6 +250,7 @@ public class Program // TODO: real bugsnag? services.AddSingleton(_ => new BugsnagNoopClient()); services.AddSingleton(); + services.AddSingleton(); services.AddMediatR(config => config.RegisterServicesFromAssemblyContaining()); services.AddMemoryCache(); diff --git a/ErsatzTV/Services/RunOnce/RebuildSearchIndexService.cs b/ErsatzTV/Services/RunOnce/RebuildSearchIndexService.cs index 804158fc2..27aeed823 100644 --- a/ErsatzTV/Services/RunOnce/RebuildSearchIndexService.cs +++ b/ErsatzTV/Services/RunOnce/RebuildSearchIndexService.cs @@ -1,5 +1,6 @@ using ErsatzTV.Application.Search; using ErsatzTV.Core; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Search; using MediatR; @@ -33,6 +34,10 @@ public class RebuildSearchIndexService : BackgroundService } using IServiceScope scope = _serviceScopeFactory.CreateScope(); + + ILanguageCodeCache languageCodeCache = scope.ServiceProvider.GetRequiredService(); + await languageCodeCache.Load(stoppingToken); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); await mediator.Send(new RebuildSearchIndex(), stoppingToken); diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 15272481c..ec20b6643 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -27,7 +27,6 @@ using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Repositories.Caching; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scripting; using ErsatzTV.Core.Interfaces.Search; @@ -52,7 +51,6 @@ using ErsatzTV.Filters; using ErsatzTV.Formatters; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data.Repositories; -using ErsatzTV.Infrastructure.Data.Repositories.Caching; using ErsatzTV.Infrastructure.Database; using ErsatzTV.Infrastructure.Emby; using ErsatzTV.Infrastructure.FFmpeg; @@ -728,6 +726,7 @@ public class Startup services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); AddChannel(services); AddChannel(services); AddChannel(services); @@ -759,7 +758,6 @@ public class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -820,6 +818,7 @@ public class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped();