Browse Source

optimize database calls related to search index (#2645)

pull/2647/head
Jason Dove 2 months ago committed by GitHub
parent
commit
d88e721d2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 22
      ErsatzTV.Application/Artists/Queries/GetArtistByIdHandler.cs
  3. 10
      ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPathHandler.cs
  4. 7
      ErsatzTV.Application/MediaCollections/Commands/AddTraktListHandler.cs
  5. 12
      ErsatzTV.Application/MediaCollections/Commands/DeleteTraktListHandler.cs
  6. 6
      ErsatzTV.Application/MediaCollections/Commands/MatchTraktListItemsHandler.cs
  7. 10
      ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs
  8. 9
      ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs
  9. 14
      ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs
  10. 33
      ErsatzTV.Application/Search/Commands/ReindexMediaItemsHandler.cs
  11. 32
      ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs
  12. 24
      ErsatzTV.Core.Tests/FFmpeg/FFmpegStreamSelectorTests.cs
  13. 9
      ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs
  14. 13
      ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
  15. 10
      ErsatzTV.Core/Interfaces/Metadata/ILanguageCodeCache.cs
  16. 8
      ErsatzTV.Core/Interfaces/Metadata/ILanguageCodeService.cs
  17. 5
      ErsatzTV.Core/Interfaces/Repositories/Caching/ICachingSearchRepository.cs
  18. 3
      ErsatzTV.Core/Interfaces/Repositories/ISearchRepository.cs
  19. 10
      ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs
  20. 48
      ErsatzTV.Core/Metadata/LanguageCodeService.cs
  21. 33
      ErsatzTV.Infrastructure.Tests/Data/Repositories/Caching/CachingSearchRepositoryTests.cs
  22. 71
      ErsatzTV.Infrastructure/Data/Repositories/Caching/CachingSearchRepository.cs
  23. 27
      ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs
  24. 646
      ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs
  25. 32
      ErsatzTV.Infrastructure/Extensions/LanguageCodeQueryableExtensions.cs
  26. 132
      ErsatzTV.Infrastructure/Extensions/QueryableExtensions.cs
  27. 59
      ErsatzTV.Infrastructure/Metadata/LanguageCodeCache.cs
  28. 137
      ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs
  29. 127
      ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs
  30. 5
      ErsatzTV.Scanner/Program.cs
  31. 5
      ErsatzTV/Services/RunOnce/RebuildSearchIndexService.cs
  32. 5
      ErsatzTV/Startup.cs

2
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 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 - 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 - 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 ### Changed
- Use smaller batch size for search index updates (100, down from 1000) - Use smaller batch size for search index updates (100, down from 1000)

22
ErsatzTV.Application/Artists/Queries/GetArtistByIdHandler.cs

@ -1,30 +1,26 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Artists.Mapper; using static ErsatzTV.Application.Artists.Mapper;
namespace ErsatzTV.Application.Artists; namespace ErsatzTV.Application.Artists;
public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<ArtistViewModel>> public class GetArtistByIdHandler(
IArtistRepository artistRepository,
ISearchRepository searchRepository,
ILanguageCodeService languageCodeService)
: IRequestHandler<GetArtistById, Option<ArtistViewModel>>
{ {
private readonly IArtistRepository _artistRepository;
private readonly ISearchRepository _searchRepository;
public GetArtistByIdHandler(IArtistRepository artistRepository, ISearchRepository searchRepository)
{
_artistRepository = artistRepository;
_searchRepository = searchRepository;
}
public async Task<Option<ArtistViewModel>> Handle( public async Task<Option<ArtistViewModel>> Handle(
GetArtistById request, GetArtistById request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
Option<Artist> maybeArtist = await _artistRepository.GetArtist(request.ArtistId); Option<Artist> maybeArtist = await artistRepository.GetArtist(request.ArtistId);
return await maybeArtist.Match<Task<Option<ArtistViewModel>>>( return await maybeArtist.Match<Task<Option<ArtistViewModel>>>(
async artist => async artist =>
{ {
List<string> mediaCodes = await _searchRepository.GetLanguagesForArtist(artist); List<string> mediaCodes = await searchRepository.GetLanguagesForArtist(artist);
List<string> languageCodes = await _searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes); List<string> languageCodes = languageCodeService.GetAllLanguageCodes(mediaCodes);
return ProjectToViewModel(artist, languageCodes); return ProjectToViewModel(artist, languageCodes);
}, },
() => Task.FromResult(Option<ArtistViewModel>.None)); () => Task.FromResult(Option<ArtistViewModel>.None));

10
ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPathHandler.cs

@ -2,7 +2,7 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata; 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.Search;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions; using ErsatzTV.Infrastructure.Extensions;
@ -15,20 +15,23 @@ public class MoveLocalLibraryPathHandler : IRequestHandler<MoveLocalLibraryPath,
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILanguageCodeService _languageCodeService;
private readonly ILogger<MoveLocalLibraryPathHandler> _logger; private readonly ILogger<MoveLocalLibraryPathHandler> _logger;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
private readonly ICachingSearchRepository _searchRepository; private readonly ISearchRepository _searchRepository;
public MoveLocalLibraryPathHandler( public MoveLocalLibraryPathHandler(
ISearchIndex searchIndex, ISearchIndex searchIndex,
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
ILogger<MoveLocalLibraryPathHandler> logger) ILogger<MoveLocalLibraryPathHandler> logger)
{ {
_searchIndex = searchIndex; _searchIndex = searchIndex;
_searchRepository = searchRepository; _searchRepository = searchRepository;
_fallbackMetadataProvider = fallbackMetadataProvider; _fallbackMetadataProvider = fallbackMetadataProvider;
_languageCodeService = languageCodeService;
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_logger = logger; _logger = logger;
} }
@ -64,6 +67,7 @@ public class MoveLocalLibraryPathHandler : IRequestHandler<MoveLocalLibraryPath,
await _searchIndex.UpdateItems( await _searchIndex.UpdateItems(
_searchRepository, _searchRepository,
_fallbackMetadataProvider, _fallbackMetadataProvider,
_languageCodeService,
[mediaItem]); [mediaItem]);
} }
} }

7
ErsatzTV.Application/MediaCollections/Commands/AddTraktListHandler.cs

@ -3,7 +3,7 @@ using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata; 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.Search;
using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Core.Interfaces.Trakt;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
@ -19,13 +19,14 @@ public partial class AddTraktListHandler : TraktCommandBase, IRequestHandler<Add
public AddTraktListHandler( public AddTraktListHandler(
ITraktApiClient traktApiClient, ITraktApiClient traktApiClient,
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
ILogger<AddTraktListHandler> logger, ILogger<AddTraktListHandler> logger,
IEntityLocker entityLocker) IEntityLocker entityLocker)
: base(traktApiClient, searchRepository, searchIndex, fallbackMetadataProvider, logger) : base(traktApiClient, searchRepository, searchIndex, fallbackMetadataProvider, languageCodeService, logger)
{ {
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_entityLocker = entityLocker; _entityLocker = entityLocker;

12
ErsatzTV.Application/MediaCollections/Commands/DeleteTraktListHandler.cs

@ -2,7 +2,7 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata; 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.Search;
using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Core.Interfaces.Trakt;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
@ -16,22 +16,25 @@ public class DeleteTraktListHandler : TraktCommandBase, IRequestHandler<DeleteTr
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEntityLocker _entityLocker; private readonly IEntityLocker _entityLocker;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILanguageCodeService _languageCodeService;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
private readonly ICachingSearchRepository _searchRepository; private readonly ISearchRepository _searchRepository;
public DeleteTraktListHandler( public DeleteTraktListHandler(
ITraktApiClient traktApiClient, ITraktApiClient traktApiClient,
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
ILogger<DeleteTraktListHandler> logger, ILogger<DeleteTraktListHandler> logger,
IEntityLocker entityLocker) IEntityLocker entityLocker)
: base(traktApiClient, searchRepository, searchIndex, fallbackMetadataProvider, logger) : base(traktApiClient, searchRepository, searchIndex, fallbackMetadataProvider, languageCodeService, logger)
{ {
_searchRepository = searchRepository; _searchRepository = searchRepository;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_fallbackMetadataProvider = fallbackMetadataProvider; _fallbackMetadataProvider = fallbackMetadataProvider;
_languageCodeService = languageCodeService;
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_entityLocker = entityLocker; _entityLocker = entityLocker;
} }
@ -65,6 +68,7 @@ public class DeleteTraktListHandler : TraktCommandBase, IRequestHandler<DeleteTr
await _searchIndex.RebuildItems( await _searchIndex.RebuildItems(
_searchRepository, _searchRepository,
_fallbackMetadataProvider, _fallbackMetadataProvider,
_languageCodeService,
mediaItemIds, mediaItemIds,
cancellationToken); cancellationToken);
} }

6
ErsatzTV.Application/MediaCollections/Commands/MatchTraktListItemsHandler.cs

@ -2,7 +2,7 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata; 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.Search;
using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Core.Interfaces.Trakt;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
@ -19,9 +19,10 @@ public class MatchTraktListItemsHandler : TraktCommandBase,
public MatchTraktListItemsHandler( public MatchTraktListItemsHandler(
ITraktApiClient traktApiClient, ITraktApiClient traktApiClient,
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
ILogger<MatchTraktListItemsHandler> logger, ILogger<MatchTraktListItemsHandler> logger,
IEntityLocker entityLocker) : base( IEntityLocker entityLocker) : base(
@ -29,6 +30,7 @@ public class MatchTraktListItemsHandler : TraktCommandBase,
searchRepository, searchRepository,
searchIndex, searchIndex,
fallbackMetadataProvider, fallbackMetadataProvider,
languageCodeService,
logger) logger)
{ {
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;

10
ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs

@ -1,7 +1,7 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata; 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.Search;
using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Core.Interfaces.Trakt;
using ErsatzTV.Core.Trakt; using ErsatzTV.Core.Trakt;
@ -15,20 +15,23 @@ namespace ErsatzTV.Application.MediaCollections;
public abstract class TraktCommandBase public abstract class TraktCommandBase
{ {
private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILanguageCodeService _languageCodeService;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
private readonly ICachingSearchRepository _searchRepository; private readonly ISearchRepository _searchRepository;
protected TraktCommandBase( protected TraktCommandBase(
ITraktApiClient traktApiClient, ITraktApiClient traktApiClient,
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
ILogger logger) ILogger logger)
{ {
_searchRepository = searchRepository; _searchRepository = searchRepository;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_fallbackMetadataProvider = fallbackMetadataProvider; _fallbackMetadataProvider = fallbackMetadataProvider;
_languageCodeService = languageCodeService;
_logger = logger; _logger = logger;
TraktApiClient = traktApiClient; TraktApiClient = traktApiClient;
@ -228,6 +231,7 @@ public abstract class TraktCommandBase
await _searchIndex.RebuildItems( await _searchIndex.RebuildItems(
_searchRepository, _searchRepository,
_fallbackMetadataProvider, _fallbackMetadataProvider,
_languageCodeService,
ids.ToList(), ids.ToList(),
cancellationToken); cancellationToken);
} }

9
ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs

@ -2,10 +2,10 @@
using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Movies.Mapper; using static ErsatzTV.Application.Movies.Mapper;
@ -15,6 +15,7 @@ public class GetMovieByIdHandler : IRequestHandler<GetMovieById, Option<MovieVie
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEmbyPathReplacementService _embyPathReplacementService; private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly ILanguageCodeService _languageCodeService;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService; private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMovieRepository _movieRepository; private readonly IMovieRepository _movieRepository;
@ -26,7 +27,8 @@ public class GetMovieByIdHandler : IRequestHandler<GetMovieById, Option<MovieVie
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
IPlexPathReplacementService plexPathReplacementService, IPlexPathReplacementService plexPathReplacementService,
IJellyfinPathReplacementService jellyfinPathReplacementService, IJellyfinPathReplacementService jellyfinPathReplacementService,
IEmbyPathReplacementService embyPathReplacementService) IEmbyPathReplacementService embyPathReplacementService,
ILanguageCodeService languageCodeService)
{ {
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_movieRepository = movieRepository; _movieRepository = movieRepository;
@ -34,6 +36,7 @@ public class GetMovieByIdHandler : IRequestHandler<GetMovieById, Option<MovieVie
_plexPathReplacementService = plexPathReplacementService; _plexPathReplacementService = plexPathReplacementService;
_jellyfinPathReplacementService = jellyfinPathReplacementService; _jellyfinPathReplacementService = jellyfinPathReplacementService;
_embyPathReplacementService = embyPathReplacementService; _embyPathReplacementService = embyPathReplacementService;
_languageCodeService = languageCodeService;
} }
public async Task<Option<MovieViewModel>> Handle( public async Task<Option<MovieViewModel>> Handle(
@ -59,7 +62,7 @@ public class GetMovieByIdHandler : IRequestHandler<GetMovieById, Option<MovieVie
.Map(ms => ms.Language) .Map(ms => ms.Language)
.ToList(); .ToList();
languageCodes.AddRange(await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCodes)); languageCodes.AddRange(_languageCodeService.GetAllLanguageCodes(mediaCodes));
} }
foreach (Movie movie in maybeMovie) foreach (Movie movie in maybeMovie)

14
ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs

@ -3,7 +3,6 @@ using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using Humanizer; using Humanizer;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -14,18 +13,20 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILanguageCodeService _languageCodeService;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<RebuildSearchIndexHandler> _logger; private readonly ILogger<RebuildSearchIndexHandler> _logger;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
private readonly ICachingSearchRepository _searchRepository; private readonly ISearchRepository _searchRepository;
private readonly SystemStartup _systemStartup; private readonly SystemStartup _systemStartup;
public RebuildSearchIndexHandler( public RebuildSearchIndexHandler(
ISearchIndex searchIndex, ISearchIndex searchIndex,
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
IConfigElementRepository configElementRepository, IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
SystemStartup systemStartup, SystemStartup systemStartup,
ILogger<RebuildSearchIndexHandler> logger) ILogger<RebuildSearchIndexHandler> logger)
{ {
@ -35,6 +36,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_fallbackMetadataProvider = fallbackMetadataProvider; _fallbackMetadataProvider = fallbackMetadataProvider;
_languageCodeService = languageCodeService;
_systemStartup = systemStartup; _systemStartup = systemStartup;
} }
@ -58,7 +60,11 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
_logger.LogInformation("Migrating search index to version {Version}", _searchIndex.Version); _logger.LogInformation("Migrating search index to version {Version}", _searchIndex.Version);
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
await _searchIndex.Rebuild(_searchRepository, _fallbackMetadataProvider, cancellationToken); await _searchIndex.Rebuild(
_searchRepository,
_fallbackMetadataProvider,
_languageCodeService,
cancellationToken);
await _configElementRepository.Upsert( await _configElementRepository.Upsert(
ConfigElementKey.SearchIndexVersion, ConfigElementKey.SearchIndexVersion,

33
ErsatzTV.Application/Search/Commands/ReindexMediaItemsHandler.cs

@ -1,28 +1,25 @@
using ErsatzTV.Core.Interfaces.Metadata; 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.Search;
namespace ErsatzTV.Application.Search; namespace ErsatzTV.Application.Search;
public class ReindexMediaItemsHandler : IRequestHandler<ReindexMediaItems> public class ReindexMediaItemsHandler(
ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
ISearchIndex searchIndex)
: IRequestHandler<ReindexMediaItems>
{ {
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) public async Task Handle(ReindexMediaItems request, CancellationToken cancellationToken)
{ {
await _searchIndex.RebuildItems(_cachingSearchRepository, _fallbackMetadataProvider, request.MediaItemIds, cancellationToken); await searchIndex.RebuildItems(
_searchIndex.Commit(); searchRepository,
fallbackMetadataProvider,
languageCodeService,
request.MediaItemIds,
cancellationToken);
searchIndex.Commit();
} }
} }

32
ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs

@ -1,4 +1,5 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions; using ErsatzTV.Infrastructure.Extensions;
@ -7,27 +8,18 @@ using static ErsatzTV.Application.Television.Mapper;
namespace ErsatzTV.Application.Television; namespace ErsatzTV.Application.Television;
public class GetTelevisionShowByIdHandler : IRequestHandler<GetTelevisionShowById, Option<TelevisionShowViewModel>> public class GetTelevisionShowByIdHandler(
IDbContextFactory<TvContext> dbContextFactory,
ISearchRepository searchRepository,
ILanguageCodeService languageCodeService,
IMediaSourceRepository mediaSourceRepository)
: IRequestHandler<GetTelevisionShowById, Option<TelevisionShowViewModel>>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchRepository _searchRepository;
public GetTelevisionShowByIdHandler(
IDbContextFactory<TvContext> dbContextFactory,
ISearchRepository searchRepository,
IMediaSourceRepository mediaSourceRepository)
{
_dbContextFactory = dbContextFactory;
_searchRepository = searchRepository;
_mediaSourceRepository = mediaSourceRepository;
}
public async Task<Option<TelevisionShowViewModel>> Handle( public async Task<Option<TelevisionShowViewModel>> Handle(
GetTelevisionShowById request, GetTelevisionShowById request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Show> maybeShow = await dbContext.Shows Option<Show> maybeShow = await dbContext.Shows
.AsNoTracking() .AsNoTracking()
@ -50,14 +42,14 @@ public class GetTelevisionShowByIdHandler : IRequestHandler<GetTelevisionShowByI
foreach (Show show in maybeShow) foreach (Show show in maybeShow)
{ {
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin(cancellationToken) Option<JellyfinMediaSource> maybeJellyfin = await mediaSourceRepository.GetAllJellyfin(cancellationToken)
.Map(list => list.HeadOrNone()); .Map(list => list.HeadOrNone());
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby(cancellationToken) Option<EmbyMediaSource> maybeEmby = await mediaSourceRepository.GetAllEmby(cancellationToken)
.Map(list => list.HeadOrNone()); .Map(list => list.HeadOrNone());
List<string> mediaCodes = await _searchRepository.GetLanguagesForShow(show); List<string> mediaCodes = await searchRepository.GetLanguagesForShow(show);
List<string> languageCodes = await _searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes); List<string> languageCodes = languageCodeService.GetAllLanguageCodes(mediaCodes);
return ProjectToViewModel(show, languageCodes, maybeJellyfin, maybeEmby); return ProjectToViewModel(show, languageCodes, maybeJellyfin, maybeEmby);
} }

24
ErsatzTV.Core.Tests/FFmpeg/FFmpegStreamSelectorTests.cs

@ -53,16 +53,16 @@ public class FFmpegStreamSelectorTests
PreferredAudioLanguageCode = "eng" PreferredAudioLanguageCode = "eng"
}; };
ISearchRepository searchRepository = Substitute.For<ISearchRepository>(); ILanguageCodeService languageCodeService = Substitute.For<ILanguageCodeService>();
searchRepository.GetAllThreeLetterLanguageCodes(Arg.Any<List<string>>()) languageCodeService.GetAllLanguageCodes(Arg.Any<List<string>>())
.Returns(Task.FromResult(new List<string> { "jpn" })); .Returns(["jpn"]);
var selector = new FFmpegStreamSelector( var selector = new FFmpegStreamSelector(
new ScriptEngine(Substitute.For<ILogger<ScriptEngine>>()), new ScriptEngine(Substitute.For<ILogger<ScriptEngine>>()),
Substitute.For<IStreamSelectorRepository>(), Substitute.For<IStreamSelectorRepository>(),
searchRepository,
Substitute.For<IConfigElementRepository>(), Substitute.For<IConfigElementRepository>(),
Substitute.For<ILocalFileSystem>(), Substitute.For<ILocalFileSystem>(),
languageCodeService,
Substitute.For<ILogger<FFmpegStreamSelector>>()); Substitute.For<ILogger<FFmpegStreamSelector>>());
Option<MediaStream> selectedStream = await selector.SelectAudioStream( Option<MediaStream> selectedStream = await selector.SelectAudioStream(
@ -115,16 +115,16 @@ public class FFmpegStreamSelectorTests
PreferredAudioTitle = "Some" PreferredAudioTitle = "Some"
}; };
ISearchRepository searchRepository = Substitute.For<ISearchRepository>(); ILanguageCodeService languageCodeService = Substitute.For<ILanguageCodeService>();
searchRepository.GetAllThreeLetterLanguageCodes(Arg.Any<List<string>>()) languageCodeService.GetAllLanguageCodes(Arg.Any<List<string>>())
.Returns(Task.FromResult(new List<string> { "jpn", "eng" })); .Returns(["jpn", "eng"]);
var selector = new FFmpegStreamSelector( var selector = new FFmpegStreamSelector(
new ScriptEngine(Substitute.For<ILogger<ScriptEngine>>()), new ScriptEngine(Substitute.For<ILogger<ScriptEngine>>()),
Substitute.For<IStreamSelectorRepository>(), Substitute.For<IStreamSelectorRepository>(),
searchRepository,
Substitute.For<IConfigElementRepository>(), Substitute.For<IConfigElementRepository>(),
Substitute.For<ILocalFileSystem>(), Substitute.For<ILocalFileSystem>(),
languageCodeService,
Substitute.For<ILogger<FFmpegStreamSelector>>()); Substitute.For<ILogger<FFmpegStreamSelector>>());
Option<MediaStream> selectedStream = await selector.SelectAudioStream( Option<MediaStream> selectedStream = await selector.SelectAudioStream(
@ -165,16 +165,16 @@ public class FFmpegStreamSelectorTests
var channel = new Channel(Guid.NewGuid()); var channel = new Channel(Guid.NewGuid());
ISearchRepository searchRepository = Substitute.For<ISearchRepository>(); ILanguageCodeService languageCodeService = Substitute.For<ILanguageCodeService>();
searchRepository.GetAllThreeLetterLanguageCodes(Arg.Any<List<string>>()) languageCodeService.GetAllLanguageCodes(Arg.Any<List<string>>())
.Returns(Task.FromResult(new List<string> { "heb" })); .Returns(["heb"]);
var selector = new FFmpegStreamSelector( var selector = new FFmpegStreamSelector(
new ScriptEngine(Substitute.For<ILogger<ScriptEngine>>()), new ScriptEngine(Substitute.For<ILogger<ScriptEngine>>()),
Substitute.For<IStreamSelectorRepository>(), Substitute.For<IStreamSelectorRepository>(),
searchRepository,
Substitute.For<IConfigElementRepository>(), Substitute.For<IConfigElementRepository>(),
Substitute.For<ILocalFileSystem>(), Substitute.For<ILocalFileSystem>(),
languageCodeService,
Substitute.For<ILogger<FFmpegStreamSelector>>()); Substitute.For<ILogger<FFmpegStreamSelector>>());
Option<Subtitle> selectedStream = await selector.SelectSubtitleStream( Option<Subtitle> selectedStream = await selector.SelectSubtitleStream(

9
ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs

@ -5,15 +5,14 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Metadata;
using ErsatzTV.Core.Scheduling; using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Infrastructure.Data.Repositories.Caching;
using ErsatzTV.Infrastructure.Extensions; using ErsatzTV.Infrastructure.Extensions;
using ErsatzTV.Infrastructure.Metadata;
using ErsatzTV.Infrastructure.Search; using ErsatzTV.Infrastructure.Search;
using ErsatzTV.Infrastructure.Sqlite.Data; using ErsatzTV.Infrastructure.Sqlite.Data;
using LanguageExt.UnsafeValueAccess; using LanguageExt.UnsafeValueAccess;
@ -86,11 +85,12 @@ public class ScheduleIntegrationTests
services.AddSingleton((Func<IServiceProvider, ILoggerFactory>)(_ => new SerilogLoggerFactory())); services.AddSingleton((Func<IServiceProvider, ILoggerFactory>)(_ => new SerilogLoggerFactory()));
services.AddScoped<ISearchRepository, SearchRepository>(); services.AddScoped<ISearchRepository, SearchRepository>();
services.AddScoped<ICachingSearchRepository, CachingSearchRepository>(); services.AddScoped<ILanguageCodeService, LanguageCodeService>();
services.AddScoped<IConfigElementRepository, ConfigElementRepository>(); services.AddScoped<IConfigElementRepository, ConfigElementRepository>();
services.AddScoped<IFallbackMetadataProvider, FallbackMetadataProvider>(); services.AddScoped<IFallbackMetadataProvider, FallbackMetadataProvider>();
services.AddSingleton<ISearchIndex, LuceneSearchIndex>(); services.AddSingleton<ISearchIndex, LuceneSearchIndex>();
services.AddSingleton<ILanguageCodeCache, LanguageCodeCache>();
services.AddSingleton(_ => Substitute.For<IClient>()); services.AddSingleton(_ => Substitute.For<IClient>());
@ -114,8 +114,9 @@ public class ScheduleIntegrationTests
_cancellationToken); _cancellationToken);
await searchIndex.Rebuild( await searchIndex.Rebuild(
provider.GetRequiredService<ICachingSearchRepository>(), provider.GetRequiredService<ISearchRepository>(),
provider.GetRequiredService<IFallbackMetadataProvider>(), provider.GetRequiredService<IFallbackMetadataProvider>(),
provider.GetRequiredService<ILanguageCodeService>(),
_cancellationToken); _cancellationToken);
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(

13
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs

@ -16,24 +16,24 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILanguageCodeService _languageCodeService;
private readonly ILogger<FFmpegStreamSelector> _logger; private readonly ILogger<FFmpegStreamSelector> _logger;
private readonly IScriptEngine _scriptEngine; private readonly IScriptEngine _scriptEngine;
private readonly ISearchRepository _searchRepository;
private readonly IStreamSelectorRepository _streamSelectorRepository; private readonly IStreamSelectorRepository _streamSelectorRepository;
public FFmpegStreamSelector( public FFmpegStreamSelector(
IScriptEngine scriptEngine, IScriptEngine scriptEngine,
IStreamSelectorRepository streamSelectorRepository, IStreamSelectorRepository streamSelectorRepository,
ISearchRepository searchRepository,
IConfigElementRepository configElementRepository, IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
ILanguageCodeService languageCodeService,
ILogger<FFmpegStreamSelector> logger) ILogger<FFmpegStreamSelector> logger)
{ {
_scriptEngine = scriptEngine; _scriptEngine = scriptEngine;
_streamSelectorRepository = streamSelectorRepository; _streamSelectorRepository = streamSelectorRepository;
_searchRepository = searchRepository;
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_languageCodeService = languageCodeService;
_logger = logger; _logger = logger;
} }
@ -73,8 +73,8 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
}); });
} }
List<string> allLanguageCodes = await _searchRepository.GetAllThreeLetterLanguageCodes([language]) List<string> allLanguageCodes =
.Map(GetTwoAndThreeLetterLanguageCodes); GetTwoAndThreeLetterLanguageCodes(_languageCodeService.GetAllLanguageCodes([language]));
if (allLanguageCodes.Count > 1) if (allLanguageCodes.Count > 1)
{ {
_logger.LogDebug("Preferred audio language has multiple codes {Codes}", allLanguageCodes); _logger.LogDebug("Preferred audio language has multiple codes {Codes}", allLanguageCodes);
@ -190,8 +190,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
else else
{ {
// filter to preferred language // filter to preferred language
allCodes = await _searchRepository.GetAllThreeLetterLanguageCodes([language]) allCodes = GetTwoAndThreeLetterLanguageCodes(_languageCodeService.GetAllLanguageCodes([language]));
.Map(GetTwoAndThreeLetterLanguageCodes);
if (allCodes.Count > 1) if (allCodes.Count > 1)
{ {
_logger.LogDebug("Preferred subtitle language has multiple codes {Codes}", allCodes); _logger.LogDebug("Preferred subtitle language has multiple codes {Codes}", allCodes);

10
ErsatzTV.Core/Interfaces/Metadata/ILanguageCodeCache.cs

@ -0,0 +1,10 @@
namespace ErsatzTV.Core.Interfaces.Metadata;
public interface ILanguageCodeCache
{
IReadOnlyDictionary<string, string[]> CodeToGroupLookup { get; }
IReadOnlyList<string[]> AllGroups { get; }
Task Load(CancellationToken cancellationToken);
}

8
ErsatzTV.Core/Interfaces/Metadata/ILanguageCodeService.cs

@ -0,0 +1,8 @@
namespace ErsatzTV.Core.Interfaces.Metadata;
public interface ILanguageCodeService
{
List<string> GetAllLanguageCodes(string mediaCode);
List<string> GetAllLanguageCodes(List<string> mediaCodes);
}

5
ErsatzTV.Core/Interfaces/Repositories/Caching/ICachingSearchRepository.cs

@ -1,5 +0,0 @@
namespace ErsatzTV.Core.Interfaces.Repositories.Caching;
public interface ICachingSearchRepository : ISearchRepository, IDisposable
{
}

3
ErsatzTV.Core/Interfaces/Repositories/ISearchRepository.cs

@ -11,6 +11,5 @@ public interface ISearchRepository
Task<List<string>> GetSubLanguagesForSeason(Season season); Task<List<string>> GetSubLanguagesForSeason(Season season);
Task<List<string>> GetLanguagesForArtist(Artist artist); Task<List<string>> GetLanguagesForArtist(Artist artist);
Task<List<string>> GetSubLanguagesForArtist(Artist artist); Task<List<string>> GetSubLanguagesForArtist(Artist artist);
Task<List<string>> GetAllThreeLetterLanguageCodes(List<string> mediaCodes); IAsyncEnumerable<MediaItem> GetAllMediaItems(CancellationToken cancellationToken);
IAsyncEnumerable<MediaItem> GetAllMediaItems();
} }

10
ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs

@ -2,7 +2,6 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Search; using ErsatzTV.Core.Search;
namespace ErsatzTV.Core.Interfaces.Search; namespace ErsatzTV.Core.Interfaces.Search;
@ -18,19 +17,22 @@ public interface ISearchIndex : IDisposable
CancellationToken cancellationToken); CancellationToken cancellationToken);
Task<Unit> Rebuild( Task<Unit> Rebuild(
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
CancellationToken cancellationToken); CancellationToken cancellationToken);
Task<Unit> RebuildItems( Task<Unit> RebuildItems(
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
IEnumerable<int> itemIds, IEnumerable<int> itemIds,
CancellationToken cancellationToken); CancellationToken cancellationToken);
Task<Unit> UpdateItems( Task<Unit> UpdateItems(
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
List<MediaItem> items); List<MediaItem> items);
Task<bool> RemoveItems(IEnumerable<int> ids); Task<bool> RemoveItems(IEnumerable<int> ids);

48
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<string> 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<string> GetAllLanguageCodes(List<string> 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<string>(validCodes);
foreach (string code in validCodes)
{
if (languageCodeCache.CodeToGroupLookup.TryGetValue(code, out string[] group))
{
result.UnionWith(group);
}
}
return result.ToList();
}
}

33
ErsatzTV.Infrastructure.Tests/Data/Repositories/Caching/CachingSearchRepositoryTests.cs

@ -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<string> { "eng" };
var frenchMediaCodes = new List<string> { "fre" };
var englishResult = new List<string> { "english_result" };
var frenchResult = new List<string> { "french_result" };
ISearchRepository searchRepo = Substitute.For<ISearchRepository>();
searchRepo.GetAllThreeLetterLanguageCodes(englishMediaCodes).Returns(englishResult.AsTask());
searchRepo.GetAllThreeLetterLanguageCodes(frenchMediaCodes).Returns(frenchResult.AsTask());
var repo = new CachingSearchRepository(searchRepo);
List<string> result1 = await repo.GetAllThreeLetterLanguageCodes(englishMediaCodes);
result1.ShouldBeEquivalentTo(englishResult);
List<string> result2 = await repo.GetAllThreeLetterLanguageCodes(frenchMediaCodes);
result2.ShouldBeEquivalentTo(frenchResult);
}
}

71
ErsatzTV.Infrastructure/Data/Repositories/Caching/CachingSearchRepository.cs

@ -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<string>, List<string>> _cache = new();
private readonly ISearchRepository _searchRepository;
private readonly SemaphoreSlim _slim = new(1, 1);
private bool _disposedValue;
public CachingSearchRepository(ISearchRepository searchRepository) => _searchRepository = searchRepository;
public Task<Option<MediaItem>> GetItemToIndex(int id, CancellationToken cancellationToken) =>
_searchRepository.GetItemToIndex(id, cancellationToken);
public Task<List<string>> GetLanguagesForShow(Show show) => _searchRepository.GetLanguagesForShow(show);
public Task<List<string>> GetSubLanguagesForShow(Show show) => _searchRepository.GetSubLanguagesForShow(show);
public Task<List<string>> GetLanguagesForSeason(Season season) => _searchRepository.GetLanguagesForSeason(season);
public Task<List<string>> GetSubLanguagesForSeason(Season season) =>
_searchRepository.GetSubLanguagesForSeason(season);
public Task<List<string>> GetLanguagesForArtist(Artist artist) => _searchRepository.GetLanguagesForArtist(artist);
public Task<List<string>> GetSubLanguagesForArtist(Artist artist) =>
_searchRepository.GetSubLanguagesForArtist(artist);
public async Task<List<string>> GetAllThreeLetterLanguageCodes(List<string> mediaCodes)
{
if (!_cache.ContainsKey(mediaCodes))
{
await _slim.WaitAsync();
try
{
_cache.TryAdd(mediaCodes, await _searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes));
}
finally
{
_slim.Release();
}
}
return _cache[mediaCodes];
}
public IAsyncEnumerable<MediaItem> 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;
}
}
}

27
ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs

@ -4,6 +4,7 @@ using Dapper;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Metadata;
using ErsatzTV.Infrastructure.Extensions; using ErsatzTV.Infrastructure.Extensions;
@ -12,15 +13,13 @@ using Microsoft.Extensions.Logging;
namespace ErsatzTV.Infrastructure.Data.Repositories; namespace ErsatzTV.Infrastructure.Data.Repositories;
public class MediaItemRepository : IMediaItemRepository public class MediaItemRepository(
IDbContextFactory<TvContext> dbContextFactory,
ILanguageCodeService languageCodeService) : IMediaItemRepository
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public MediaItemRepository(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public async Task<List<CultureInfo>> GetAllKnownCultures() public async Task<List<CultureInfo>> GetAllKnownCultures()
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
var result = new System.Collections.Generic.HashSet<CultureInfo>(); var result = new System.Collections.Generic.HashSet<CultureInfo>();
@ -44,7 +43,7 @@ public class MediaItemRepository : IMediaItemRepository
public async Task<List<LanguageCodeAndName>> GetAllLanguageCodesAndNames() public async Task<List<LanguageCodeAndName>> GetAllLanguageCodesAndNames()
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
var result = new System.Collections.Generic.HashSet<LanguageCodeAndName>(); var result = new System.Collections.Generic.HashSet<LanguageCodeAndName>();
@ -53,7 +52,7 @@ public class MediaItemRepository : IMediaItemRepository
var unseenCodes = new System.Collections.Generic.HashSet<string>(mediaCodes); var unseenCodes = new System.Collections.Generic.HashSet<string>(mediaCodes);
foreach (string mediaCode in mediaCodes) foreach (string mediaCode in mediaCodes)
{ {
foreach (string code in await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCode)) foreach (string code in languageCodeService.GetAllLanguageCodes(mediaCode))
{ {
Option<CultureInfo> maybeCulture = allCultures.Find(c => string.Equals( Option<CultureInfo> maybeCulture = allCultures.Find(c => string.Equals(
code, code,
@ -81,7 +80,7 @@ public class MediaItemRepository : IMediaItemRepository
public async Task<List<int>> FlagFileNotFound(LibraryPath libraryPath, string path) public async Task<List<int>> FlagFileNotFound(LibraryPath libraryPath, string path)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
List<int> ids = await dbContext.Connection.QueryAsync<int>( List<int> ids = await dbContext.Connection.QueryAsync<int>(
@"SELECT M.Id @"SELECT M.Id
@ -101,7 +100,7 @@ public class MediaItemRepository : IMediaItemRepository
public async Task<ImmutableHashSet<string>> GetAllTrashedItems(LibraryPath libraryPath) public async Task<ImmutableHashSet<string>> GetAllTrashedItems(LibraryPath libraryPath)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QueryAsync<string>( return await dbContext.Connection.QueryAsync<string>(
@"SELECT MF.Path @"SELECT MF.Path
FROM MediaItem M FROM MediaItem M
@ -114,7 +113,7 @@ public class MediaItemRepository : IMediaItemRepository
public async Task SetInterlacedRatio(MediaItem mediaItem, double interlacedRatio) 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(); var mediaVersion = mediaItem.GetHeadVersion();
mediaVersion.InterlacedRatio = interlacedRatio; mediaVersion.InterlacedRatio = interlacedRatio;
@ -126,7 +125,7 @@ public class MediaItemRepository : IMediaItemRepository
public async Task<Unit> FlagNormal(MediaItem mediaItem) public async Task<Unit> FlagNormal(MediaItem mediaItem)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
mediaItem.State = MediaItemState.Normal; mediaItem.State = MediaItemState.Normal;
@ -137,7 +136,7 @@ public class MediaItemRepository : IMediaItemRepository
public async Task<Either<BaseError, Unit>> DeleteItems(List<int> mediaItemIds) public async Task<Either<BaseError, Unit>> DeleteItems(List<int> mediaItemIds)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
foreach (int mediaItemId in mediaItemIds) foreach (int mediaItemId in mediaItemIds)
{ {
@ -229,7 +228,7 @@ public class MediaItemRepository : IMediaItemRepository
private async Task<List<string>> GetAllLanguageCodes() private async Task<List<string>> GetAllLanguageCodes()
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QueryAsync<string>( return await dbContext.Connection.QueryAsync<string>(
@"SELECT LanguageCode FROM @"SELECT LanguageCode FROM
(SELECT Language AS LanguageCode (SELECT Language AS LanguageCode

646
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.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Extensions; using ErsatzTV.Infrastructure.Extensions;
@ -6,183 +7,91 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Infrastructure.Data.Repositories; namespace ErsatzTV.Infrastructure.Data.Repositories;
public class SearchRepository : ISearchRepository public class SearchRepository(IDbContextFactory<TvContext> dbContextFactory) : ISearchRepository
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public SearchRepository(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public async Task<Option<MediaItem>> GetItemToIndex(int id, CancellationToken cancellationToken) public async Task<Option<MediaItem>> GetItemToIndex(int id, CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.MediaItems
var baseItem = await dbContext.MediaItems
.AsNoTracking() .AsNoTracking()
.Include(mi => mi.Collections) .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
.Include(mi => mi.LibraryPath)
.ThenInclude(lp => lp.Library) if (baseItem is null)
.Include(mi => (mi as Movie).MovieMetadata) {
.ThenInclude(mm => mm.Genres) return Option<MediaItem>.None;
.Include(mi => (mi as Movie).MovieMetadata) }
.ThenInclude(mm => mm.Tags)
.Include(mi => (mi as Movie).MovieMetadata) switch (baseItem)
.ThenInclude(mm => mm.Studios) {
.Include(mi => (mi as Movie).MovieMetadata) case Movie:
.ThenInclude(mm => mm.Actors) return await dbContext.Movies
.Include(mi => (mi as Movie).MovieMetadata) .AsNoTracking()
.ThenInclude(mm => mm.Directors) .IncludeForSearch()
.Include(mi => (mi as Movie).MovieMetadata) .AsSplitQuery()
.ThenInclude(mm => mm.Writers) .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
.Include(mi => (mi as Movie).MovieMetadata) case Episode:
.ThenInclude(em => em.Guids) return await dbContext.Episodes
.Include(mi => (mi as Movie).MediaVersions) .AsNoTracking()
.ThenInclude(mv => mv.Chapters) .IncludeForSearch()
.Include(mi => (mi as Movie).MediaVersions) .AsSplitQuery()
.ThenInclude(mm => mm.Streams) .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
.Include(mi => (mi as Episode).EpisodeMetadata) case Season:
.ThenInclude(em => em.Genres) return await dbContext.Seasons
.Include(mi => (mi as Episode).EpisodeMetadata) .AsNoTracking()
.ThenInclude(em => em.Tags) .IncludeForSearch()
.Include(mi => (mi as Episode).EpisodeMetadata) .AsSplitQuery()
.ThenInclude(em => em.Studios) .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
.Include(mi => (mi as Episode).EpisodeMetadata) case Show:
.ThenInclude(em => em.Actors) return await dbContext.Shows
.Include(mi => (mi as Episode).EpisodeMetadata) .AsNoTracking()
.ThenInclude(em => em.Directors) .IncludeForSearch()
.Include(mi => (mi as Episode).EpisodeMetadata) .AsSplitQuery()
.ThenInclude(em => em.Writers) .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
.Include(mi => (mi as Episode).EpisodeMetadata) case MusicVideo:
.ThenInclude(em => em.Guids) return await dbContext.MusicVideos
.Include(mi => (mi as Episode).MediaVersions) .AsNoTracking()
.ThenInclude(mv => mv.Chapters) .IncludeForSearch()
.Include(mi => (mi as Episode).MediaVersions) .AsSplitQuery()
.ThenInclude(em => em.Streams) .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
.Include(mi => (mi as Episode).MediaVersions) case Artist:
.ThenInclude(em => em.MediaFiles) return await dbContext.Artists
.Include(mi => (mi as Episode).Season) .AsNoTracking()
.ThenInclude(s => s.Show) .IncludeForSearch()
.ThenInclude(s => s.ShowMetadata) .AsSplitQuery()
.ThenInclude(s => s.Genres) .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
.Include(mi => (mi as Episode).Season) case OtherVideo:
.ThenInclude(s => s.Show) return await dbContext.OtherVideos
.ThenInclude(s => s.ShowMetadata) .AsNoTracking()
.ThenInclude(s => s.Tags) .IncludeForSearch()
.Include(mi => (mi as Episode).Season) .AsSplitQuery()
.ThenInclude(s => s.Show) .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
.ThenInclude(s => s.ShowMetadata) case Song:
.ThenInclude(s => s.Studios) return await dbContext.Songs
.Include(mi => (mi as Season).SeasonMetadata) .AsNoTracking()
.ThenInclude(sm => sm.Genres) .IncludeForSearch()
.Include(mi => (mi as Season).SeasonMetadata) .AsSplitQuery()
.ThenInclude(sm => sm.Tags) .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
.Include(mi => (mi as Season).SeasonMetadata) case Image:
.ThenInclude(sm => sm.Studios) return await dbContext.Images
.Include(mi => (mi as Season).SeasonMetadata) .AsNoTracking()
.ThenInclude(sm => sm.Actors) .IncludeForSearch()
.Include(mi => (mi as Season).SeasonMetadata) .AsSplitQuery()
.ThenInclude(sm => sm.Guids) .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
.Include(mi => (mi as Season).Show) case RemoteStream:
.ThenInclude(sm => sm.ShowMetadata) return await dbContext.RemoteStreams
.ThenInclude(sm => sm.Genres) .AsNoTracking()
.Include(mi => (mi as Season).Show) .IncludeForSearch()
.ThenInclude(sm => sm.ShowMetadata) .AsSplitQuery()
.ThenInclude(sm => sm.Tags) .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
.Include(mi => (mi as Season).Show) }
.ThenInclude(sm => sm.ShowMetadata)
.ThenInclude(sm => sm.Studios) return Option<MediaItem>.None;
.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);
} }
public async Task<List<string>> GetLanguagesForShow(Show show) public async Task<List<string>> GetLanguagesForShow(Show show)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QueryAsync<string>( return await dbContext.Connection.QueryAsync<string>(
@"SELECT DISTINCT Language @"SELECT DISTINCT Language
FROM MediaStream FROM MediaStream
@ -195,7 +104,7 @@ public class SearchRepository : ISearchRepository
public async Task<List<string>> GetSubLanguagesForShow(Show show) public async Task<List<string>> GetSubLanguagesForShow(Show show)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QueryAsync<string>( return await dbContext.Connection.QueryAsync<string>(
@"SELECT DISTINCT Language @"SELECT DISTINCT Language
FROM MediaStream FROM MediaStream
@ -208,7 +117,7 @@ public class SearchRepository : ISearchRepository
public async Task<List<string>> GetLanguagesForSeason(Season season) public async Task<List<string>> GetLanguagesForSeason(Season season)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QueryAsync<string>( return await dbContext.Connection.QueryAsync<string>(
@"SELECT DISTINCT Language @"SELECT DISTINCT Language
FROM MediaStream FROM MediaStream
@ -220,7 +129,7 @@ public class SearchRepository : ISearchRepository
public async Task<List<string>> GetSubLanguagesForSeason(Season season) public async Task<List<string>> GetSubLanguagesForSeason(Season season)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QueryAsync<string>( return await dbContext.Connection.QueryAsync<string>(
@"SELECT DISTINCT Language @"SELECT DISTINCT Language
FROM MediaStream FROM MediaStream
@ -232,7 +141,7 @@ public class SearchRepository : ISearchRepository
public async Task<List<string>> GetLanguagesForArtist(Artist artist) public async Task<List<string>> GetLanguagesForArtist(Artist artist)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QueryAsync<string>( return await dbContext.Connection.QueryAsync<string>(
@"SELECT DISTINCT Language @"SELECT DISTINCT Language
FROM MediaStream FROM MediaStream
@ -245,7 +154,7 @@ public class SearchRepository : ISearchRepository
public async Task<List<string>> GetSubLanguagesForArtist(Artist artist) public async Task<List<string>> GetSubLanguagesForArtist(Artist artist)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QueryAsync<string>( return await dbContext.Connection.QueryAsync<string>(
@"SELECT DISTINCT Language @"SELECT DISTINCT Language
FROM MediaStream FROM MediaStream
@ -256,177 +165,230 @@ public class SearchRepository : ISearchRepository
new { ArtistId = artist.Id }).Map(result => result.ToList()); new { ArtistId = artist.Id }).Map(result => result.ToList());
} }
public virtual async Task<List<string>> GetAllThreeLetterLanguageCodes(List<string> mediaCodes) public async IAsyncEnumerable<MediaItem> 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<Movie> GetAllMovies([EnumeratorCancellation] CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCodes);
ConfiguredCancelableAsyncEnumerable<Movie> movies = dbContext.Movies
.AsNoTracking()
.IncludeForSearch()
.AsSplitQuery()
.AsAsyncEnumerable()
.WithCancellation(cancellationToken);
await foreach (var movie in movies)
{
yield return movie;
}
}
private async IAsyncEnumerable<Show> GetAllShows([EnumeratorCancellation] CancellationToken cancellationToken)
{
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
ConfiguredCancelableAsyncEnumerable<Show> shows = dbContext.Shows
.AsNoTracking()
.IncludeForSearch()
.AsSplitQuery()
.AsAsyncEnumerable()
.WithCancellation(cancellationToken);
await foreach (var movie in shows)
{
yield return movie;
}
} }
public IAsyncEnumerable<MediaItem> GetAllMediaItems() private async IAsyncEnumerable<Season> GetAllSeasons([EnumeratorCancellation] CancellationToken cancellationToken)
{ {
TvContext dbContext = _dbContextFactory.CreateDbContext(); await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return dbContext.MediaItems
ConfiguredCancelableAsyncEnumerable<Season> seasons = dbContext.Seasons
.AsNoTracking() .AsNoTracking()
.Include(mi => mi.Collections) .IncludeForSearch()
.Include(mi => mi.LibraryPath) .AsSplitQuery()
.ThenInclude(lp => lp.Library) .AsAsyncEnumerable()
.Include(mi => (mi as Movie).MovieMetadata) .WithCancellation(cancellationToken);
.ThenInclude(mm => mm.Genres)
.Include(mi => (mi as Movie).MovieMetadata) await foreach (var movie in seasons)
.ThenInclude(mm => mm.Tags) {
.Include(mi => (mi as Movie).MovieMetadata) yield return movie;
.ThenInclude(mm => mm.Studios) }
.Include(mi => (mi as Movie).MovieMetadata) }
.ThenInclude(mm => mm.Actors)
.Include(mi => (mi as Movie).MovieMetadata) private async IAsyncEnumerable<Episode> GetAllEpisodes([EnumeratorCancellation] CancellationToken cancellationToken)
.ThenInclude(mm => mm.Directors) {
.Include(mi => (mi as Movie).MovieMetadata) await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
.ThenInclude(mm => mm.Writers)
.Include(mi => (mi as Movie).MovieMetadata) ConfiguredCancelableAsyncEnumerable<Episode> episodes = dbContext.Episodes
.ThenInclude(mm => mm.Guids) .AsNoTracking()
.Include(mi => (mi as Movie).MediaVersions) .IncludeForSearch()
.ThenInclude(mv => mv.Chapters) .AsSplitQuery()
.Include(mi => (mi as Movie).MediaVersions) .AsAsyncEnumerable()
.ThenInclude(mm => mm.Streams) .WithCancellation(cancellationToken);
.Include(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Genres) await foreach (var movie in episodes)
.Include(mi => (mi as Episode).EpisodeMetadata) {
.ThenInclude(em => em.Tags) yield return movie;
.Include(mi => (mi as Episode).EpisodeMetadata) }
.ThenInclude(em => em.Studios) }
.Include(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Actors) private async IAsyncEnumerable<MusicVideo> GetAllMusicVideos(
.Include(mi => (mi as Episode).EpisodeMetadata) [EnumeratorCancellation] CancellationToken cancellationToken)
.ThenInclude(em => em.Directors) {
.Include(mi => (mi as Episode).EpisodeMetadata) await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
.ThenInclude(em => em.Writers)
.Include(mi => (mi as Episode).EpisodeMetadata) ConfiguredCancelableAsyncEnumerable<MusicVideo> musicVideos = dbContext.MusicVideos
.ThenInclude(em => em.Guids) .AsNoTracking()
.Include(mi => (mi as Episode).MediaVersions) .IncludeForSearch()
.ThenInclude(mv => mv.Chapters) .AsSplitQuery()
.Include(mi => (mi as Episode).MediaVersions) .AsAsyncEnumerable()
.ThenInclude(em => em.Streams) .WithCancellation(cancellationToken);
.Include(mi => (mi as Episode).MediaVersions)
.ThenInclude(em => em.MediaFiles) await foreach (var movie in musicVideos)
.Include(mi => (mi as Episode).Season) {
.ThenInclude(s => s.Show) yield return movie;
.ThenInclude(s => s.ShowMetadata) }
.ThenInclude(s => s.Genres) }
.Include(mi => (mi as Episode).Season)
.ThenInclude(s => s.Show) private async IAsyncEnumerable<Artist> GetAllArtists([EnumeratorCancellation] CancellationToken cancellationToken)
.ThenInclude(s => s.ShowMetadata) {
.ThenInclude(s => s.Tags) await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
.Include(mi => (mi as Episode).Season)
.ThenInclude(s => s.Show) ConfiguredCancelableAsyncEnumerable<Artist> artists = dbContext.Artists
.ThenInclude(s => s.ShowMetadata) .AsNoTracking()
.ThenInclude(s => s.Studios) .IncludeForSearch()
.Include(mi => (mi as Season).SeasonMetadata) .AsSplitQuery()
.ThenInclude(sm => sm.Genres) .AsAsyncEnumerable()
.Include(mi => (mi as Season).SeasonMetadata) .WithCancellation(cancellationToken);
.ThenInclude(sm => sm.Tags)
.Include(mi => (mi as Season).SeasonMetadata) await foreach (var movie in artists)
.ThenInclude(sm => sm.Studios) {
.Include(mi => (mi as Season).SeasonMetadata) yield return movie;
.ThenInclude(sm => sm.Actors) }
.Include(mi => (mi as Season).SeasonMetadata) }
.ThenInclude(sm => sm.Guids)
.Include(mi => (mi as Season).Show) private async IAsyncEnumerable<OtherVideo> GetAllOtherVideos(
.ThenInclude(sm => sm.ShowMetadata) [EnumeratorCancellation] CancellationToken cancellationToken)
.ThenInclude(sm => sm.Genres) {
.Include(mi => (mi as Season).Show) await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
.ThenInclude(sm => sm.ShowMetadata)
.ThenInclude(sm => sm.Tags) ConfiguredCancelableAsyncEnumerable<OtherVideo> otherVideos = dbContext.OtherVideos
.Include(mi => (mi as Season).Show) .AsNoTracking()
.ThenInclude(sm => sm.ShowMetadata) .IncludeForSearch()
.ThenInclude(sm => sm.Studios) .AsSplitQuery()
.Include(mi => (mi as Show).ShowMetadata) .AsAsyncEnumerable()
.ThenInclude(mm => mm.Genres) .WithCancellation(cancellationToken);
.Include(mi => (mi as Show).ShowMetadata)
.ThenInclude(mm => mm.Tags) await foreach (var movie in otherVideos)
.Include(mi => (mi as Show).ShowMetadata) {
.ThenInclude(mm => mm.Studios) yield return movie;
.Include(mi => (mi as Show).ShowMetadata) }
.ThenInclude(mm => mm.Actors) }
.Include(mi => (mi as Show).ShowMetadata)
.ThenInclude(mm => mm.Guids) private async IAsyncEnumerable<Song> GetAllSongs([EnumeratorCancellation] CancellationToken cancellationToken)
.Include(mi => (mi as MusicVideo).Artist) {
.ThenInclude(mm => mm.ArtistMetadata) await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
.Include(mi => (mi as MusicVideo).MusicVideoMetadata)
.ThenInclude(mm => mm.Artists) ConfiguredCancelableAsyncEnumerable<Song> songs = dbContext.Songs
.Include(mi => (mi as MusicVideo).MusicVideoMetadata) .AsNoTracking()
.ThenInclude(mm => mm.Genres) .IncludeForSearch()
.Include(mi => (mi as MusicVideo).MusicVideoMetadata) .AsSplitQuery()
.ThenInclude(mm => mm.Tags) .AsAsyncEnumerable()
.Include(mi => (mi as MusicVideo).MusicVideoMetadata) .WithCancellation(cancellationToken);
.ThenInclude(mm => mm.Studios)
.Include(mi => (mi as MusicVideo).MusicVideoMetadata) await foreach (var movie in songs)
.ThenInclude(mm => mm.Guids) {
.Include(mi => (mi as MusicVideo).MediaVersions) yield return movie;
.ThenInclude(mv => mv.Chapters) }
.Include(mi => (mi as MusicVideo).MediaVersions) }
.ThenInclude(mm => mm.Streams)
.Include(mi => (mi as Artist).ArtistMetadata) private async IAsyncEnumerable<Image> GetAllImages([EnumeratorCancellation] CancellationToken cancellationToken)
.ThenInclude(mm => mm.Genres) {
.Include(mi => (mi as Artist).ArtistMetadata) await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
.ThenInclude(mm => mm.Styles)
.Include(mi => (mi as Artist).ArtistMetadata) ConfiguredCancelableAsyncEnumerable<Image> images = dbContext.Images
.ThenInclude(mm => mm.Moods) .AsNoTracking()
.Include(mi => (mi as Artist).ArtistMetadata) .IncludeForSearch()
.ThenInclude(mm => mm.Guids) .AsSplitQuery()
.Include(mi => (mi as OtherVideo).OtherVideoMetadata) .AsAsyncEnumerable()
.ThenInclude(ovm => ovm.Genres) .WithCancellation(cancellationToken);
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
.ThenInclude(ovm => ovm.Tags) await foreach (var movie in images)
.Include(mi => (mi as OtherVideo).OtherVideoMetadata) {
.ThenInclude(ovm => ovm.Studios) yield return movie;
.Include(mi => (mi as OtherVideo).OtherVideoMetadata) }
.ThenInclude(ovm => ovm.Actors) }
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
.ThenInclude(ovm => ovm.Directors) private async IAsyncEnumerable<RemoteStream> GetAllRemoteStreams(
.Include(mi => (mi as OtherVideo).OtherVideoMetadata) [EnumeratorCancellation] CancellationToken cancellationToken)
.ThenInclude(ovm => ovm.Writers) {
.Include(mi => (mi as OtherVideo).OtherVideoMetadata) await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
.ThenInclude(ovm => ovm.Guids)
.Include(mi => (mi as OtherVideo).MediaVersions) ConfiguredCancelableAsyncEnumerable<RemoteStream> remoteStreams = dbContext.RemoteStreams
.ThenInclude(mv => mv.Chapters) .AsNoTracking()
.Include(mi => (mi as OtherVideo).MediaVersions) .IncludeForSearch()
.ThenInclude(ovm => ovm.Streams) .AsSplitQuery()
.Include(mi => (mi as Song).SongMetadata) .AsAsyncEnumerable()
.ThenInclude(mm => mm.Tags) .WithCancellation(cancellationToken);
.Include(mi => (mi as Song).SongMetadata)
.ThenInclude(mm => mm.Genres) await foreach (var movie in remoteStreams)
.Include(mi => (mi as Song).SongMetadata) {
.ThenInclude(mm => mm.Guids) yield return movie;
.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();
} }
} }

32
ErsatzTV.Infrastructure/Extensions/LanguageCodeQueryableExtensions.cs

@ -5,38 +5,6 @@ namespace ErsatzTV.Infrastructure.Extensions;
public static class LanguageCodeQueryableExtensions public static class LanguageCodeQueryableExtensions
{ {
public static async Task<List<string>> GetAllLanguageCodes(
this IQueryable<LanguageCode> languageCodes,
string mediaCode)
{
if (string.IsNullOrWhiteSpace(mediaCode))
{
return new List<string>();
}
string code = mediaCode.ToLowerInvariant();
List<LanguageCode> maybeLanguages = await languageCodes
.Filter(lc => lc.ThreeCode1 == code || lc.ThreeCode2 == code)
.ToListAsync();
var result = new System.Collections.Generic.HashSet<string>();
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<List<string>> GetAllLanguageCodes( public static async Task<List<string>> GetAllLanguageCodes(
this IQueryable<LanguageCode> languageCodes, this IQueryable<LanguageCode> languageCodes,
List<string> mediaCodes) List<string> mediaCodes)

132
ErsatzTV.Infrastructure/Extensions/QueryableExtensions.cs

@ -1,4 +1,5 @@
using System.Linq.Expressions; using System.Linq.Expressions;
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Infrastructure.Extensions; namespace ErsatzTV.Infrastructure.Extensions;
@ -11,4 +12,135 @@ public static class QueryableExtensions
Expression<Func<T, bool>> predicate, Expression<Func<T, bool>> predicate,
CancellationToken cancellationToken) where T : class => CancellationToken cancellationToken) where T : class =>
await enumerable.OrderBy(keySelector).FirstOrDefaultAsync(predicate, cancellationToken); await enumerable.OrderBy(keySelector).FirstOrDefaultAsync(predicate, cancellationToken);
public static IQueryable<Movie> IncludeForSearch(this IQueryable<Movie> 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<Episode> IncludeForSearch(this IQueryable<Episode> 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<Season> IncludeForSearch(this IQueryable<Season> 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<Show> IncludeForSearch(this IQueryable<Show> 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<MusicVideo> IncludeForSearch(this IQueryable<MusicVideo> 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<Artist> IncludeForSearch(this IQueryable<Artist> 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<OtherVideo> IncludeForSearch(this IQueryable<OtherVideo> 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<Song> IncludeForSearch(this IQueryable<Song> 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<Image> IncludeForSearch(this IQueryable<Image> 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<RemoteStream> IncludeForSearch(this IQueryable<RemoteStream> 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);
} }

59
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<string, string[]> CodeToGroupLookup { get; private set; }
public IReadOnlyList<string[]> AllGroups { get; private set; }
public async Task Load(CancellationToken cancellationToken)
{
var lookup = new Dictionary<string, string[]>();
var allGroups = new List<string[]>();
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;
}
}

137
ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs

@ -6,7 +6,6 @@ using Elastic.Clients.Elasticsearch.IndexManagement;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Search; using ErsatzTV.Core.Search;
using ErsatzTV.FFmpeg; using ErsatzTV.FFmpeg;
@ -69,8 +68,9 @@ public class ElasticSearchIndex : ISearchIndex
} }
public async Task<Unit> Rebuild( public async Task<Unit> Rebuild(
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
DeleteIndexResponse deleteResponse = await _client.Indices.DeleteAsync(IndexName, cancellationToken); DeleteIndexResponse deleteResponse = await _client.Indices.DeleteAsync(IndexName, cancellationToken);
@ -85,17 +85,18 @@ public class ElasticSearchIndex : ISearchIndex
return Unit.Default; 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; return Unit.Default;
} }
public async Task<Unit> RebuildItems( public async Task<Unit> RebuildItems(
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
IEnumerable<int> itemIds, IEnumerable<int> itemIds,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
@ -103,7 +104,7 @@ public class ElasticSearchIndex : ISearchIndex
{ {
foreach (MediaItem mediaItem in await searchRepository.GetItemToIndex(id, cancellationToken)) 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<Unit> UpdateItems( public async Task<Unit> UpdateItems(
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
List<MediaItem> items) List<MediaItem> items)
{ {
foreach (MediaItem item in items) foreach (MediaItem item in items)
@ -120,31 +122,31 @@ public class ElasticSearchIndex : ISearchIndex
switch (item) switch (item)
{ {
case Movie movie: case Movie movie:
await UpdateMovie(searchRepository, movie); await UpdateMovie(languageCodeService, movie);
break; break;
case Show show: case Show show:
await UpdateShow(searchRepository, show); await UpdateShow(searchRepository, languageCodeService, show);
break; break;
case Season season: case Season season:
await UpdateSeason(searchRepository, season); await UpdateSeason(searchRepository, languageCodeService, season);
break; break;
case Artist artist: case Artist artist:
await UpdateArtist(searchRepository, artist); await UpdateArtist(searchRepository, languageCodeService, artist);
break; break;
case MusicVideo musicVideo: case MusicVideo musicVideo:
await UpdateMusicVideo(searchRepository, musicVideo); await UpdateMusicVideo(languageCodeService, musicVideo);
break; break;
case Episode episode: case Episode episode:
await UpdateEpisode(searchRepository, fallbackMetadataProvider, episode); await UpdateEpisode(languageCodeService, fallbackMetadataProvider, episode);
break; break;
case OtherVideo otherVideo: case OtherVideo otherVideo:
await UpdateOtherVideo(searchRepository, otherVideo); await UpdateOtherVideo(languageCodeService, otherVideo);
break; break;
case Song song: case Song song:
await UpdateSong(searchRepository, song); await UpdateSong(languageCodeService, song);
break; break;
case Image image: case Image image:
await UpdateImage(searchRepository, image); await UpdateImage(languageCodeService, image);
break; break;
} }
} }
@ -266,38 +268,39 @@ public class ElasticSearchIndex : ISearchIndex
private async Task RebuildItem( private async Task RebuildItem(
ISearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
MediaItem mediaItem) MediaItem mediaItem)
{ {
switch (mediaItem) switch (mediaItem)
{ {
case Movie movie: case Movie movie:
await UpdateMovie(searchRepository, movie); await UpdateMovie(languageCodeService, movie);
break; break;
case Show show: case Show show:
await UpdateShow(searchRepository, show); await UpdateShow(searchRepository, languageCodeService, show);
break; break;
case Season season: case Season season:
await UpdateSeason(searchRepository, season); await UpdateSeason(searchRepository, languageCodeService, season);
break; break;
case Artist artist: case Artist artist:
await UpdateArtist(searchRepository, artist); await UpdateArtist(searchRepository, languageCodeService, artist);
break; break;
case MusicVideo musicVideo: case MusicVideo musicVideo:
await UpdateMusicVideo(searchRepository, musicVideo); await UpdateMusicVideo(languageCodeService, musicVideo);
break; break;
case Episode episode: case Episode episode:
await UpdateEpisode(searchRepository, fallbackMetadataProvider, episode); await UpdateEpisode(languageCodeService, fallbackMetadataProvider, episode);
break; break;
case OtherVideo otherVideo: case OtherVideo otherVideo:
await UpdateOtherVideo(searchRepository, otherVideo); await UpdateOtherVideo(languageCodeService, otherVideo);
break; break;
case Song song: case Song song:
await UpdateSong(searchRepository, song); await UpdateSong(languageCodeService, song);
break; break;
} }
} }
private async Task UpdateMovie(ISearchRepository searchRepository, Movie movie) private async Task UpdateMovie(ILanguageCodeService languageCodeService, Movie movie)
{ {
foreach (MovieMetadata metadata in movie.MovieMetadata.HeadOrNone()) foreach (MovieMetadata metadata in movie.MovieMetadata.HeadOrNone())
{ {
@ -315,9 +318,9 @@ public class ElasticSearchIndex : ISearchIndex
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = movie.State.ToString(), State = movie.State.ToString(),
MetadataKind = metadata.MetadataKind.ToString(), MetadataKind = metadata.MetadataKind.ToString(),
Language = await GetLanguages(searchRepository, movie.MediaVersions), Language = GetLanguages(languageCodeService, movie.MediaVersions),
LanguageTag = GetLanguageTags(movie.MediaVersions), LanguageTag = GetLanguageTags(movie.MediaVersions),
SubLanguage = await GetSubLanguages(searchRepository, movie.MediaVersions), SubLanguage = GetSubLanguages(languageCodeService, movie.MediaVersions),
SubLanguageTag = GetSubLanguageTags(movie.MediaVersions), SubLanguageTag = GetSubLanguageTags(movie.MediaVersions),
ContentRating = GetContentRatings(metadata.ContentRating), ContentRating = GetContentRatings(metadata.ContentRating),
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), 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()) foreach (ShowMetadata metadata in show.ShowMetadata.HeadOrNone())
{ {
@ -374,10 +377,10 @@ public class ElasticSearchIndex : ISearchIndex
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = show.State.ToString(), State = show.State.ToString(),
MetadataKind = metadata.MetadataKind.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), LanguageTag = await searchRepository.GetLanguagesForShow(show),
SubLanguage = await GetLanguages( SubLanguage = GetLanguages(
searchRepository, languageCodeService,
await searchRepository.GetSubLanguagesForShow(show)), await searchRepository.GetSubLanguagesForShow(show)),
SubLanguageTag = await searchRepository.GetSubLanguagesForShow(show), SubLanguageTag = await searchRepository.GetSubLanguagesForShow(show),
ContentRating = GetContentRatings(metadata.ContentRating), 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 (SeasonMetadata metadata in season.SeasonMetadata.HeadOrNone())
foreach (ShowMetadata showMetadata in season.Show.ShowMetadata.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(), ShowTag = showMetadata.Tags.Map(t => t.Name).ToList(),
ShowStudio = showMetadata.Studios.Map(s => s.Name).ToList(), ShowStudio = showMetadata.Studios.Map(s => s.Name).ToList(),
ShowContentRating = GetContentRatings(showMetadata.ContentRating), ShowContentRating = GetContentRatings(showMetadata.ContentRating),
Language = await GetLanguages( Language = GetLanguages(
searchRepository, languageCodeService,
await searchRepository.GetLanguagesForSeason(season)), await searchRepository.GetLanguagesForSeason(season)),
LanguageTag = await searchRepository.GetLanguagesForSeason(season), LanguageTag = await searchRepository.GetLanguagesForSeason(season),
SubLanguage = await GetLanguages( SubLanguage = GetLanguages(
searchRepository, languageCodeService,
await searchRepository.GetSubLanguagesForSeason(season)), await searchRepository.GetSubLanguagesForSeason(season)),
SubLanguageTag = await searchRepository.GetSubLanguagesForSeason(season), SubLanguageTag = await searchRepository.GetSubLanguagesForSeason(season),
ContentRating = GetContentRatings(showMetadata.ContentRating), 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()) foreach (ArtistMetadata metadata in artist.ArtistMetadata.HeadOrNone())
{ {
@ -492,12 +501,12 @@ public class ElasticSearchIndex : ISearchIndex
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = artist.State.ToString(), State = artist.State.ToString(),
MetadataKind = metadata.MetadataKind.ToString(), MetadataKind = metadata.MetadataKind.ToString(),
Language = await GetLanguages( Language = GetLanguages(
searchRepository, languageCodeService,
await searchRepository.GetLanguagesForArtist(artist)), await searchRepository.GetLanguagesForArtist(artist)),
LanguageTag = await searchRepository.GetLanguagesForArtist(artist), LanguageTag = await searchRepository.GetLanguagesForArtist(artist),
SubLanguage = await GetLanguages( SubLanguage = GetLanguages(
searchRepository, languageCodeService,
await searchRepository.GetSubLanguagesForArtist(artist)), await searchRepository.GetSubLanguagesForArtist(artist)),
SubLanguageTag = await searchRepository.GetSubLanguagesForArtist(artist), SubLanguageTag = await searchRepository.GetSubLanguagesForArtist(artist),
AddedDate = GetAddedDate(metadata.DateAdded), 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()) foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata.HeadOrNone())
{ {
@ -539,9 +548,9 @@ public class ElasticSearchIndex : ISearchIndex
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = musicVideo.State.ToString(), State = musicVideo.State.ToString(),
MetadataKind = metadata.MetadataKind.ToString(), MetadataKind = metadata.MetadataKind.ToString(),
Language = await GetLanguages(searchRepository, musicVideo.MediaVersions), Language = GetLanguages(languageCodeService, musicVideo.MediaVersions),
LanguageTag = GetLanguageTags(musicVideo.MediaVersions), LanguageTag = GetLanguageTags(musicVideo.MediaVersions),
SubLanguage = await GetSubLanguages(searchRepository, musicVideo.MediaVersions), SubLanguage = GetSubLanguages(languageCodeService, musicVideo.MediaVersions),
SubLanguageTag = GetSubLanguageTags(musicVideo.MediaVersions), SubLanguageTag = GetSubLanguageTags(musicVideo.MediaVersions),
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), ReleaseDate = GetReleaseDate(metadata.ReleaseDate),
AddedDate = GetAddedDate(metadata.DateAdded), AddedDate = GetAddedDate(metadata.DateAdded),
@ -589,7 +598,7 @@ public class ElasticSearchIndex : ISearchIndex
} }
private async Task UpdateEpisode( private async Task UpdateEpisode(
ISearchRepository searchRepository, ILanguageCodeService languageCodeService,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
Episode episode) Episode episode)
{ {
@ -622,9 +631,9 @@ public class ElasticSearchIndex : ISearchIndex
MetadataKind = metadata.MetadataKind.ToString(), MetadataKind = metadata.MetadataKind.ToString(),
SeasonNumber = episode.Season?.SeasonNumber ?? 0, SeasonNumber = episode.Season?.SeasonNumber ?? 0,
EpisodeNumber = metadata.EpisodeNumber, EpisodeNumber = metadata.EpisodeNumber,
Language = await GetLanguages(searchRepository, episode.MediaVersions), Language = GetLanguages(languageCodeService, episode.MediaVersions),
LanguageTag = GetLanguageTags(episode.MediaVersions), LanguageTag = GetLanguageTags(episode.MediaVersions),
SubLanguage = await GetSubLanguages(searchRepository, episode.MediaVersions), SubLanguage = GetSubLanguages(languageCodeService, episode.MediaVersions),
SubLanguageTag = GetSubLanguageTags(episode.MediaVersions), SubLanguageTag = GetSubLanguageTags(episode.MediaVersions),
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), ReleaseDate = GetReleaseDate(metadata.ReleaseDate),
AddedDate = GetAddedDate(metadata.DateAdded), 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()) foreach (OtherVideoMetadata metadata in otherVideo.OtherVideoMetadata.HeadOrNone())
{ {
@ -689,9 +698,9 @@ public class ElasticSearchIndex : ISearchIndex
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = otherVideo.State.ToString(), State = otherVideo.State.ToString(),
MetadataKind = metadata.MetadataKind.ToString(), MetadataKind = metadata.MetadataKind.ToString(),
Language = await GetLanguages(searchRepository, otherVideo.MediaVersions), Language = GetLanguages(languageCodeService, otherVideo.MediaVersions),
LanguageTag = GetLanguageTags(otherVideo.MediaVersions), LanguageTag = GetLanguageTags(otherVideo.MediaVersions),
SubLanguage = await GetSubLanguages(searchRepository, otherVideo.MediaVersions), SubLanguage = GetSubLanguages(languageCodeService, otherVideo.MediaVersions),
SubLanguageTag = GetSubLanguageTags(otherVideo.MediaVersions), SubLanguageTag = GetSubLanguageTags(otherVideo.MediaVersions),
ContentRating = GetContentRatings(metadata.ContentRating), ContentRating = GetContentRatings(metadata.ContentRating),
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), 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()) foreach (SongMetadata metadata in song.SongMetadata.HeadOrNone())
{ {
@ -745,9 +754,9 @@ public class ElasticSearchIndex : ISearchIndex
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = song.State.ToString(), State = song.State.ToString(),
MetadataKind = metadata.MetadataKind.ToString(), MetadataKind = metadata.MetadataKind.ToString(),
Language = await GetLanguages(searchRepository, song.MediaVersions), Language = GetLanguages(languageCodeService, song.MediaVersions),
LanguageTag = GetLanguageTags(song.MediaVersions), LanguageTag = GetLanguageTags(song.MediaVersions),
SubLanguage = await GetSubLanguages(searchRepository, song.MediaVersions), SubLanguage = GetSubLanguages(languageCodeService, song.MediaVersions),
SubLanguageTag = GetSubLanguageTags(song.MediaVersions), SubLanguageTag = GetSubLanguageTags(song.MediaVersions),
AddedDate = GetAddedDate(metadata.DateAdded), AddedDate = GetAddedDate(metadata.DateAdded),
Album = metadata.Album ?? string.Empty, 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()) foreach (ImageMetadata metadata in image.ImageMetadata.HeadOrNone())
{ {
@ -794,9 +803,9 @@ public class ElasticSearchIndex : ISearchIndex
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = image.State.ToString(), State = image.State.ToString(),
MetadataKind = metadata.MetadataKind.ToString(), MetadataKind = metadata.MetadataKind.ToString(),
Language = await GetLanguages(searchRepository, image.MediaVersions), Language = GetLanguages(languageCodeService, image.MediaVersions),
LanguageTag = GetLanguageTags(image.MediaVersions), LanguageTag = GetLanguageTags(image.MediaVersions),
SubLanguage = await GetSubLanguages(searchRepository, image.MediaVersions), SubLanguage = GetSubLanguages(languageCodeService, image.MediaVersions),
SubLanguageTag = GetSubLanguageTags(image.MediaVersions), SubLanguageTag = GetSubLanguageTags(image.MediaVersions),
AddedDate = GetAddedDate(metadata.DateAdded), AddedDate = GetAddedDate(metadata.DateAdded),
Genre = metadata.Genres.Map(g => g.Name).ToList(), Genre = metadata.Genres.Map(g => g.Name).ToList(),
@ -853,9 +862,7 @@ public class ElasticSearchIndex : ISearchIndex
return contentRatings; return contentRatings;
} }
private async Task<List<string>> GetLanguages( private List<string> GetLanguages(ILanguageCodeService languageCodeService, IEnumerable<MediaVersion> mediaVersions)
ISearchRepository searchRepository,
IEnumerable<MediaVersion> mediaVersions)
{ {
var result = new List<string>(); var result = new List<string>();
@ -867,14 +874,14 @@ public class ElasticSearchIndex : ISearchIndex
.Distinct() .Distinct()
.ToList(); .ToList();
result.AddRange(await GetLanguages(searchRepository, mediaCodes)); result.AddRange(GetLanguages(languageCodeService, mediaCodes));
} }
return result; return result;
} }
private async Task<List<string>> GetSubLanguages( private List<string> GetSubLanguages(
ISearchRepository searchRepository, ILanguageCodeService languageCodeService,
IEnumerable<MediaVersion> mediaVersions) IEnumerable<MediaVersion> mediaVersions)
{ {
var result = new List<string>(); var result = new List<string>();
@ -887,16 +894,16 @@ public class ElasticSearchIndex : ISearchIndex
.Distinct() .Distinct()
.ToList(); .ToList();
result.AddRange(await GetLanguages(searchRepository, mediaCodes)); result.AddRange(GetLanguages(languageCodeService, mediaCodes));
} }
return result; return result;
} }
private async Task<List<string>> GetLanguages(ISearchRepository searchRepository, List<string> mediaCodes) private List<string> GetLanguages(ILanguageCodeService languageCodeService, List<string> mediaCodes)
{ {
var englishNames = new System.Collections.Generic.HashSet<string>(); var englishNames = new System.Collections.Generic.HashSet<string>();
foreach (string code in await searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes)) foreach (string code in languageCodeService.GetAllLanguageCodes(mediaCodes))
{ {
Option<CultureInfo> maybeCultureInfo = _cultureInfos.Find(ci => string.Equals( Option<CultureInfo> maybeCultureInfo = _cultureInfos.Find(ci => string.Equals(
ci.ThreeLetterISOLanguageName, ci.ThreeLetterISOLanguageName,

127
ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs

@ -4,7 +4,6 @@ using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Search; using ErsatzTV.Core.Search;
using ErsatzTV.FFmpeg; using ErsatzTV.FFmpeg;
@ -155,8 +154,9 @@ public sealed class LuceneSearchIndex : ISearchIndex
} }
public async Task<Unit> UpdateItems( public async Task<Unit> UpdateItems(
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
List<MediaItem> items) List<MediaItem> items)
{ {
foreach (MediaItem item in items) foreach (MediaItem item in items)
@ -164,34 +164,34 @@ public sealed class LuceneSearchIndex : ISearchIndex
switch (item) switch (item)
{ {
case Movie movie: case Movie movie:
await UpdateMovie(searchRepository, movie); UpdateMovie(languageCodeService, movie);
break; break;
case Show show: case Show show:
await UpdateShow(searchRepository, show); await UpdateShow(searchRepository, languageCodeService, show);
break; break;
case Season season: case Season season:
await UpdateSeason(searchRepository, season); await UpdateSeason(searchRepository, languageCodeService, season);
break; break;
case Artist artist: case Artist artist:
await UpdateArtist(searchRepository, artist); await UpdateArtist(searchRepository, languageCodeService, artist);
break; break;
case MusicVideo musicVideo: case MusicVideo musicVideo:
await UpdateMusicVideo(searchRepository, musicVideo); UpdateMusicVideo(languageCodeService, musicVideo);
break; break;
case Episode episode: case Episode episode:
await UpdateEpisode(searchRepository, fallbackMetadataProvider, episode); UpdateEpisode(languageCodeService, fallbackMetadataProvider, episode);
break; break;
case OtherVideo otherVideo: case OtherVideo otherVideo:
await UpdateOtherVideo(searchRepository, otherVideo); UpdateOtherVideo(languageCodeService, otherVideo);
break; break;
case Song song: case Song song:
await UpdateSong(searchRepository, song); UpdateSong(languageCodeService, song);
break; break;
case Image image: case Image image:
await UpdateImage(searchRepository, image); UpdateImage(languageCodeService, image);
break; break;
case RemoteStream remoteStream: case RemoteStream remoteStream:
await UpdateRemoteStream(searchRepository, remoteStream); UpdateRemoteStream(languageCodeService, remoteStream);
break; break;
} }
} }
@ -275,16 +275,17 @@ public sealed class LuceneSearchIndex : ISearchIndex
} }
public async Task<Unit> Rebuild( public async Task<Unit> Rebuild(
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
_writer.DeleteAll(); _writer.DeleteAll();
_writer.Commit(); _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(); _writer.Commit();
@ -292,8 +293,9 @@ public sealed class LuceneSearchIndex : ISearchIndex
} }
public async Task<Unit> RebuildItems( public async Task<Unit> RebuildItems(
ICachingSearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
IEnumerable<int> itemIds, IEnumerable<int> itemIds,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
@ -301,7 +303,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
{ {
foreach (MediaItem mediaItem in await searchRepository.GetItemToIndex(id, cancellationToken)) 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( private async Task RebuildItem(
ISearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILanguageCodeService languageCodeService,
MediaItem mediaItem) MediaItem mediaItem)
{ {
switch (mediaItem) switch (mediaItem)
{ {
case Movie movie: case Movie movie:
await UpdateMovie(searchRepository, movie); UpdateMovie(languageCodeService, movie);
break; break;
case Show show: case Show show:
await UpdateShow(searchRepository, show); await UpdateShow(searchRepository, languageCodeService, show);
break; break;
case Season season: case Season season:
await UpdateSeason(searchRepository, season); await UpdateSeason(searchRepository, languageCodeService, season);
break; break;
case Artist artist: case Artist artist:
await UpdateArtist(searchRepository, artist); await UpdateArtist(searchRepository, languageCodeService, artist);
break; break;
case MusicVideo musicVideo: case MusicVideo musicVideo:
await UpdateMusicVideo(searchRepository, musicVideo); UpdateMusicVideo(languageCodeService, musicVideo);
break; break;
case Episode episode: case Episode episode:
await UpdateEpisode(searchRepository, fallbackMetadataProvider, episode); UpdateEpisode(languageCodeService, fallbackMetadataProvider, episode);
break; break;
case OtherVideo otherVideo: case OtherVideo otherVideo:
await UpdateOtherVideo(searchRepository, otherVideo); UpdateOtherVideo(languageCodeService, otherVideo);
break; break;
case Song song: case Song song:
await UpdateSong(searchRepository, song); UpdateSong(languageCodeService, song);
break; break;
case Image image: case Image image:
await UpdateImage(searchRepository, image); UpdateImage(languageCodeService, image);
break; break;
case RemoteStream remoteStream: case RemoteStream remoteStream:
await UpdateRemoteStream(searchRepository, remoteStream); UpdateRemoteStream(languageCodeService, remoteStream);
break; break;
} }
} }
@ -421,7 +424,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
return new SearchPageMap(map); return new SearchPageMap(map);
} }
private async Task UpdateMovie(ISearchRepository searchRepository, Movie movie) private void UpdateMovie(ILanguageCodeService languageCodeService, Movie movie)
{ {
Option<MovieMetadata> maybeMetadata = movie.MovieMetadata.HeadOrNone(); Option<MovieMetadata> maybeMetadata = movie.MovieMetadata.HeadOrNone();
if (maybeMetadata.IsSome) if (maybeMetadata.IsSome)
@ -447,7 +450,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO)
}; };
await AddLanguages(searchRepository, doc, movie.MediaVersions); AddLanguages(languageCodeService, doc, movie.MediaVersions);
AddStatistics(doc, movie.MediaVersions); AddStatistics(doc, movie.MediaVersions);
AddCollections(doc, movie.Collections); AddCollections(doc, movie.Collections);
@ -540,8 +543,8 @@ public sealed class LuceneSearchIndex : ISearchIndex
} }
} }
private async Task AddLanguages( private void AddLanguages(
ISearchRepository searchRepository, ILanguageCodeService languageCodeService,
Document doc, Document doc,
ICollection<MediaVersion> mediaVersions) ICollection<MediaVersion> mediaVersions)
{ {
@ -552,7 +555,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
.Distinct() .Distinct()
.ToList(); .ToList();
await AddLanguages(searchRepository, doc, mediaCodes); AddLanguages(languageCodeService, doc, mediaCodes);
var subMediaCodes = mediaVersions var subMediaCodes = mediaVersions
.Map(mv => mv.Streams .Map(mv => mv.Streams
@ -563,10 +566,10 @@ public sealed class LuceneSearchIndex : ISearchIndex
.Distinct() .Distinct()
.ToList(); .ToList();
await AddSubLanguages(searchRepository, doc, subMediaCodes); AddSubLanguages(languageCodeService, doc, subMediaCodes);
} }
private async Task AddLanguages(ISearchRepository searchRepository, Document doc, List<string> mediaCodes) private void AddLanguages(ILanguageCodeService languageCodeService, Document doc, List<string> mediaCodes)
{ {
foreach (string code in mediaCodes.Where(c => !string.IsNullOrWhiteSpace(c)).Distinct()) 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<string>(); var englishNames = new System.Collections.Generic.HashSet<string>();
foreach (string code in await searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes)) foreach (string code in languageCodeService.GetAllLanguageCodes(mediaCodes))
{ {
Option<CultureInfo> maybeCultureInfo = _cultureInfos.Find(ci => string.Equals( Option<CultureInfo> maybeCultureInfo = _cultureInfos.Find(ci => string.Equals(
ci.ThreeLetterISOLanguageName, ci.ThreeLetterISOLanguageName,
@ -592,7 +595,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
} }
} }
private async Task AddSubLanguages(ISearchRepository searchRepository, Document doc, List<string> mediaCodes) private void AddSubLanguages(ILanguageCodeService languageCodeService, Document doc, List<string> mediaCodes)
{ {
foreach (string code in mediaCodes.Where(c => !string.IsNullOrWhiteSpace(c)).Distinct()) 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<string>(); var englishNames = new System.Collections.Generic.HashSet<string>();
foreach (string code in await searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes)) foreach (string code in languageCodeService.GetAllLanguageCodes(mediaCodes))
{ {
Option<CultureInfo> maybeCultureInfo = _cultureInfos.Find(ci => string.Equals( Option<CultureInfo> maybeCultureInfo = _cultureInfos.Find(ci => string.Equals(
ci.ThreeLetterISOLanguageName, 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<ShowMetadata> maybeMetadata = show.ShowMetadata.HeadOrNone(); Option<ShowMetadata> maybeMetadata = show.ShowMetadata.HeadOrNone();
if (maybeMetadata.IsSome) if (maybeMetadata.IsSome)
@ -645,10 +648,10 @@ public sealed class LuceneSearchIndex : ISearchIndex
}; };
List<string> languages = await searchRepository.GetLanguagesForShow(show); List<string> languages = await searchRepository.GetLanguagesForShow(show);
await AddLanguages(searchRepository, doc, languages); AddLanguages(languageCodeService, doc, languages);
List<string> subLanguages = await searchRepository.GetSubLanguagesForShow(show); List<string> subLanguages = await searchRepository.GetSubLanguagesForShow(show);
await AddSubLanguages(searchRepository, doc, subLanguages); AddSubLanguages(languageCodeService, doc, subLanguages);
AddCollections(doc, show.Collections); 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<SeasonMetadata> maybeMetadata = season.SeasonMetadata.HeadOrNone(); Option<SeasonMetadata> maybeMetadata = season.SeasonMetadata.HeadOrNone();
Option<ShowMetadata> maybeShowMetadata = season.Show.ShowMetadata.HeadOrNone(); Option<ShowMetadata> maybeShowMetadata = season.Show.ShowMetadata.HeadOrNone();
@ -791,10 +797,10 @@ public sealed class LuceneSearchIndex : ISearchIndex
} }
List<string> languages = await searchRepository.GetLanguagesForSeason(season); List<string> languages = await searchRepository.GetLanguagesForSeason(season);
await AddLanguages(searchRepository, doc, languages); AddLanguages(languageCodeService, doc, languages);
List<string> subLanguages = await searchRepository.GetSubLanguagesForSeason(season); List<string> subLanguages = await searchRepository.GetSubLanguagesForSeason(season);
await AddSubLanguages(searchRepository, doc, subLanguages); AddSubLanguages(languageCodeService, doc, subLanguages);
AddCollections(doc, season.Collections); 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<ArtistMetadata> maybeMetadata = artist.ArtistMetadata.HeadOrNone(); Option<ArtistMetadata> maybeMetadata = artist.ArtistMetadata.HeadOrNone();
if (maybeMetadata.IsSome) if (maybeMetadata.IsSome)
@ -875,10 +884,10 @@ public sealed class LuceneSearchIndex : ISearchIndex
}; };
List<string> languages = await searchRepository.GetLanguagesForArtist(artist); List<string> languages = await searchRepository.GetLanguagesForArtist(artist);
await AddLanguages(searchRepository, doc, languages); AddLanguages(languageCodeService, doc, languages);
List<string> subLanguages = await searchRepository.GetSubLanguagesForArtist(artist); List<string> subLanguages = await searchRepository.GetSubLanguagesForArtist(artist);
await AddSubLanguages(searchRepository, doc, subLanguages); AddSubLanguages(languageCodeService, doc, subLanguages);
AddCollections(doc, artist.Collections); 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<MusicVideoMetadata> maybeMetadata = musicVideo.MusicVideoMetadata.HeadOrNone(); Option<MusicVideoMetadata> maybeMetadata = musicVideo.MusicVideoMetadata.HeadOrNone();
if (maybeMetadata.IsSome) if (maybeMetadata.IsSome)
@ -944,7 +953,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO)
}; };
await AddLanguages(searchRepository, doc, musicVideo.MediaVersions); AddLanguages(languageCodeService, doc, musicVideo.MediaVersions);
AddStatistics(doc, musicVideo.MediaVersions); AddStatistics(doc, musicVideo.MediaVersions);
AddCollections(doc, musicVideo.Collections); AddCollections(doc, musicVideo.Collections);
@ -1022,8 +1031,8 @@ public sealed class LuceneSearchIndex : ISearchIndex
} }
} }
private async Task UpdateEpisode( private void UpdateEpisode(
ISearchRepository searchRepository, ILanguageCodeService languageCodeService,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
Episode episode) Episode episode)
{ {
@ -1106,7 +1115,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
doc.Add(new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO)); 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); AddStatistics(doc, episode.MediaVersions);
AddCollections(doc, episode.Collections); 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<OtherVideoMetadata> maybeMetadata = otherVideo.OtherVideoMetadata.HeadOrNone(); Option<OtherVideoMetadata> maybeMetadata = otherVideo.OtherVideoMetadata.HeadOrNone();
if (maybeMetadata.IsSome) if (maybeMetadata.IsSome)
@ -1209,7 +1218,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO)
}; };
await AddLanguages(searchRepository, doc, otherVideo.MediaVersions); AddLanguages(languageCodeService, doc, otherVideo.MediaVersions);
AddStatistics(doc, otherVideo.MediaVersions); AddStatistics(doc, otherVideo.MediaVersions);
AddCollections(doc, otherVideo.Collections); 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<SongMetadata> maybeMetadata = song.SongMetadata.HeadOrNone(); Option<SongMetadata> maybeMetadata = song.SongMetadata.HeadOrNone();
foreach (var metadata in maybeMetadata) foreach (var metadata in maybeMetadata)
@ -1313,7 +1322,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO)
}; };
await AddLanguages(searchRepository, doc, song.MediaVersions); AddLanguages(languageCodeService, doc, song.MediaVersions);
AddStatistics(doc, song.MediaVersions); AddStatistics(doc, song.MediaVersions);
AddCollections(doc, song.Collections); 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<ImageMetadata> maybeMetadata = image.ImageMetadata.HeadOrNone(); Option<ImageMetadata> maybeMetadata = image.ImageMetadata.HeadOrNone();
if (maybeMetadata.IsSome) if (maybeMetadata.IsSome)
@ -1401,7 +1410,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
Field.Store.NO)); Field.Store.NO));
} }
await AddLanguages(searchRepository, doc, image.MediaVersions); AddLanguages(languageCodeService, doc, image.MediaVersions);
AddStatistics(doc, image.MediaVersions); AddStatistics(doc, image.MediaVersions);
AddCollections(doc, image.Collections); 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<RemoteStreamMetadata> maybeMetadata = remoteStream.RemoteStreamMetadata.HeadOrNone(); Option<RemoteStreamMetadata> maybeMetadata = remoteStream.RemoteStreamMetadata.HeadOrNone();
if (maybeMetadata.IsSome) if (maybeMetadata.IsSome)
@ -1474,7 +1483,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
Field.Store.NO)); Field.Store.NO));
} }
await AddLanguages(searchRepository, doc, remoteStream.MediaVersions); AddLanguages(languageCodeService, doc, remoteStream.MediaVersions);
AddStatistics(doc, remoteStream.MediaVersions); AddStatistics(doc, remoteStream.MediaVersions);
AddCollections(doc, remoteStream.Collections); AddCollections(doc, remoteStream.Collections);

5
ErsatzTV.Scanner/Program.cs

@ -11,7 +11,6 @@ using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Jellyfin; using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Metadata;
@ -21,7 +20,6 @@ using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Infrastructure.Data.Repositories.Caching;
using ErsatzTV.Infrastructure.Emby; using ErsatzTV.Infrastructure.Emby;
using ErsatzTV.Infrastructure.Images; using ErsatzTV.Infrastructure.Images;
using ErsatzTV.Infrastructure.Jellyfin; using ErsatzTV.Infrastructure.Jellyfin;
@ -185,7 +183,7 @@ public class Program
services.AddScoped<IRemoteStreamRepository, RemoteStreamRepository>(); services.AddScoped<IRemoteStreamRepository, RemoteStreamRepository>();
services.AddScoped<ILibraryRepository, LibraryRepository>(); services.AddScoped<ILibraryRepository, LibraryRepository>();
services.AddScoped<ISearchRepository, SearchRepository>(); services.AddScoped<ISearchRepository, SearchRepository>();
services.AddScoped<ICachingSearchRepository, CachingSearchRepository>(); services.AddScoped<ILanguageCodeService, LanguageCodeService>();
services.AddScoped<ILocalMetadataProvider, LocalMetadataProvider>(); services.AddScoped<ILocalMetadataProvider, LocalMetadataProvider>();
services.AddScoped<IFallbackMetadataProvider, FallbackMetadataProvider>(); services.AddScoped<IFallbackMetadataProvider, FallbackMetadataProvider>();
services.AddScoped<ILocalStatisticsProvider, LocalStatisticsProvider>(); services.AddScoped<ILocalStatisticsProvider, LocalStatisticsProvider>();
@ -252,6 +250,7 @@ public class Program
// TODO: real bugsnag? // TODO: real bugsnag?
services.AddSingleton<IClient>(_ => new BugsnagNoopClient()); services.AddSingleton<IClient>(_ => new BugsnagNoopClient());
services.AddSingleton<IScannerProxy, ScannerProxy>(); services.AddSingleton<IScannerProxy, ScannerProxy>();
services.AddSingleton<ILanguageCodeCache, LanguageCodeCache>();
services.AddMediatR(config => config.RegisterServicesFromAssemblyContaining<Worker>()); services.AddMediatR(config => config.RegisterServicesFromAssemblyContaining<Worker>());
services.AddMemoryCache(); services.AddMemoryCache();

5
ErsatzTV/Services/RunOnce/RebuildSearchIndexService.cs

@ -1,5 +1,6 @@
using ErsatzTV.Application.Search; using ErsatzTV.Application.Search;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Search; using ErsatzTV.Core.Search;
using MediatR; using MediatR;
@ -33,6 +34,10 @@ public class RebuildSearchIndexService : BackgroundService
} }
using IServiceScope scope = _serviceScopeFactory.CreateScope(); using IServiceScope scope = _serviceScopeFactory.CreateScope();
ILanguageCodeCache languageCodeCache = scope.ServiceProvider.GetRequiredService<ILanguageCodeCache>();
await languageCodeCache.Load(stoppingToken);
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>(); IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
await mediator.Send(new RebuildSearchIndex(), stoppingToken); await mediator.Send(new RebuildSearchIndex(), stoppingToken);

5
ErsatzTV/Startup.cs

@ -27,7 +27,6 @@ using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Interfaces.Scripting; using ErsatzTV.Core.Interfaces.Scripting;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
@ -52,7 +51,6 @@ using ErsatzTV.Filters;
using ErsatzTV.Formatters; using ErsatzTV.Formatters;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Infrastructure.Data.Repositories.Caching;
using ErsatzTV.Infrastructure.Database; using ErsatzTV.Infrastructure.Database;
using ErsatzTV.Infrastructure.Emby; using ErsatzTV.Infrastructure.Emby;
using ErsatzTV.Infrastructure.FFmpeg; using ErsatzTV.Infrastructure.FFmpeg;
@ -728,6 +726,7 @@ public class Startup
services.AddSingleton<IHlsPlaylistFilter, HlsPlaylistFilter>(); services.AddSingleton<IHlsPlaylistFilter, HlsPlaylistFilter>();
services.AddSingleton<RecyclableMemoryStreamManager>(); services.AddSingleton<RecyclableMemoryStreamManager>();
services.AddSingleton<SystemStartup>(); services.AddSingleton<SystemStartup>();
services.AddSingleton<ILanguageCodeCache, LanguageCodeCache>();
AddChannel<IBackgroundServiceRequest>(services); AddChannel<IBackgroundServiceRequest>(services);
AddChannel<IPlexBackgroundServiceRequest>(services); AddChannel<IPlexBackgroundServiceRequest>(services);
AddChannel<IJellyfinBackgroundServiceRequest>(services); AddChannel<IJellyfinBackgroundServiceRequest>(services);
@ -759,7 +758,6 @@ public class Startup
services.AddScoped<IConfigElementRepository, ConfigElementRepository>(); services.AddScoped<IConfigElementRepository, ConfigElementRepository>();
services.AddScoped<ITelevisionRepository, TelevisionRepository>(); services.AddScoped<ITelevisionRepository, TelevisionRepository>();
services.AddScoped<ISearchRepository, SearchRepository>(); services.AddScoped<ISearchRepository, SearchRepository>();
services.AddScoped<ICachingSearchRepository, CachingSearchRepository>();
services.AddScoped<IMovieRepository, MovieRepository>(); services.AddScoped<IMovieRepository, MovieRepository>();
services.AddScoped<IArtistRepository, ArtistRepository>(); services.AddScoped<IArtistRepository, ArtistRepository>();
services.AddScoped<IMusicVideoRepository, MusicVideoRepository>(); services.AddScoped<IMusicVideoRepository, MusicVideoRepository>();
@ -820,6 +818,7 @@ public class Startup
services.AddScoped<IGraphicsElementSelector, GraphicsElementSelector>(); services.AddScoped<IGraphicsElementSelector, GraphicsElementSelector>();
services.AddScoped<IHlsInitSegmentCache, HlsInitSegmentCache>(); services.AddScoped<IHlsInitSegmentCache, HlsInitSegmentCache>();
services.AddScoped<IMpegTsScriptService, MpegTsScriptService>(); services.AddScoped<IMpegTsScriptService, MpegTsScriptService>();
services.AddScoped<ILanguageCodeService, LanguageCodeService>();
services.AddScoped<IFFmpegProcessService, FFmpegLibraryProcessService>(); services.AddScoped<IFFmpegProcessService, FFmpegLibraryProcessService>();
services.AddScoped<IPipelineBuilderFactory, PipelineBuilderFactory>(); services.AddScoped<IPipelineBuilderFactory, PipelineBuilderFactory>();

Loading…
Cancel
Save