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. 31
      ErsatzTV.Application/Search/Commands/ReindexMediaItemsHandler.cs
  11. 28
      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. 644
      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/). @@ -77,6 +77,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix bug where looping motion graphics wouldn't be displayed when seeking into second half of content
- Fix `content_total_duration` value in graphics engine opacity expressions
- This bug caused some graphics elements to display too early after first joining a channel
- Optimize database calls made for search index rebuilds and updates
- This should improve performance of library scans
### Changed
- Use smaller batch size for search index updates (100, down from 1000)

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

@ -1,30 +1,26 @@ @@ -1,30 +1,26 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Artists.Mapper;
namespace ErsatzTV.Application.Artists;
public class GetArtistByIdHandler : IRequestHandler<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(
GetArtistById request,
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>>>(
async artist =>
{
List<string> mediaCodes = await _searchRepository.GetLanguagesForArtist(artist);
List<string> languageCodes = await _searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes);
List<string> mediaCodes = await searchRepository.GetLanguagesForArtist(artist);
List<string> languageCodes = languageCodeService.GetAllLanguageCodes(mediaCodes);
return ProjectToViewModel(artist, languageCodes);
},
() => Task.FromResult(Option<ArtistViewModel>.None));

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

13
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs

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

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

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

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

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

48
ErsatzTV.Core/Metadata/LanguageCodeService.cs

@ -0,0 +1,48 @@ @@ -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 @@ @@ -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 @@ @@ -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; @@ -4,6 +4,7 @@ using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Infrastructure.Extensions;
@ -12,15 +13,13 @@ using Microsoft.Extensions.Logging; @@ -12,15 +13,13 @@ using Microsoft.Extensions.Logging;
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()
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
var result = new System.Collections.Generic.HashSet<CultureInfo>();
@ -44,7 +43,7 @@ public class MediaItemRepository : IMediaItemRepository @@ -44,7 +43,7 @@ public class MediaItemRepository : IMediaItemRepository
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>();
@ -53,7 +52,7 @@ public class MediaItemRepository : IMediaItemRepository @@ -53,7 +52,7 @@ public class MediaItemRepository : IMediaItemRepository
var unseenCodes = new System.Collections.Generic.HashSet<string>(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(
code,
@ -81,7 +80,7 @@ public class MediaItemRepository : IMediaItemRepository @@ -81,7 +80,7 @@ public class MediaItemRepository : IMediaItemRepository
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>(
@"SELECT M.Id
@ -101,7 +100,7 @@ public class MediaItemRepository : IMediaItemRepository @@ -101,7 +100,7 @@ public class MediaItemRepository : IMediaItemRepository
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>(
@"SELECT MF.Path
FROM MediaItem M
@ -114,7 +113,7 @@ public class MediaItemRepository : IMediaItemRepository @@ -114,7 +113,7 @@ public class MediaItemRepository : IMediaItemRepository
public async Task SetInterlacedRatio(MediaItem mediaItem, double interlacedRatio)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
var mediaVersion = mediaItem.GetHeadVersion();
mediaVersion.InterlacedRatio = interlacedRatio;
@ -126,7 +125,7 @@ public class MediaItemRepository : IMediaItemRepository @@ -126,7 +125,7 @@ public class MediaItemRepository : IMediaItemRepository
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;
@ -137,7 +136,7 @@ public class MediaItemRepository : IMediaItemRepository @@ -137,7 +136,7 @@ public class MediaItemRepository : IMediaItemRepository
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)
{
@ -229,7 +228,7 @@ public class MediaItemRepository : IMediaItemRepository @@ -229,7 +228,7 @@ public class MediaItemRepository : IMediaItemRepository
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>(
@"SELECT LanguageCode FROM
(SELECT Language AS LanguageCode

644
ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs

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

32
ErsatzTV.Infrastructure/Extensions/LanguageCodeQueryableExtensions.cs

@ -5,38 +5,6 @@ namespace ErsatzTV.Infrastructure.Extensions; @@ -5,38 +5,6 @@ namespace ErsatzTV.Infrastructure.Extensions;
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(
this IQueryable<LanguageCode> languageCodes,
List<string> mediaCodes)

132
ErsatzTV.Infrastructure/Extensions/QueryableExtensions.cs

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

127
ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs

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

5
ErsatzTV.Scanner/Program.cs

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

5
ErsatzTV/Services/RunOnce/RebuildSearchIndexService.cs

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

5
ErsatzTV/Startup.cs

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

Loading…
Cancel
Save