Browse Source

fix missing trashed episodes (#881)

* fix episodes missing from trash

* cleanup
pull/882/head
Jason Dove 3 years ago committed by GitHub
parent
commit
9c02a6738b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPathHandler.cs
  2. 4
      ErsatzTV.Application/MediaCollections/Commands/AddTraktListHandler.cs
  3. 11
      ErsatzTV.Application/MediaCollections/Commands/DeleteTraktListHandler.cs
  4. 9
      ErsatzTV.Application/MediaCollections/Commands/MatchTraktListItemsHandler.cs
  5. 6
      ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs
  6. 5
      ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs
  7. 48
      ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs
  8. 7
      ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs
  9. 2
      ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs
  10. 6
      ErsatzTV.Core/Emby/EmbyCollectionScanner.cs
  11. 2
      ErsatzTV.Core/Emby/EmbyMovieLibraryScanner.cs
  12. 2
      ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs
  13. 15
      ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs
  14. 6
      ErsatzTV.Core/Jellyfin/JellyfinCollectionScanner.cs
  15. 2
      ErsatzTV.Core/Jellyfin/JellyfinMovieLibraryScanner.cs
  16. 2
      ErsatzTV.Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs
  17. 8
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  18. 12
      ErsatzTV.Core/Metadata/MediaServerMovieLibraryScanner.cs
  19. 26
      ErsatzTV.Core/Metadata/MediaServerTelevisionLibraryScanner.cs
  20. 10
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  21. 15
      ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs
  22. 10
      ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs
  23. 10
      ErsatzTV.Core/Metadata/SongFolderScanner.cs
  24. 21
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  25. 2
      ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs
  26. 2
      ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs
  27. 3
      ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs
  28. 26
      ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs
  29. 45
      ErsatzTV.Infrastructure/Search/SearchIndex.cs

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

@ -1,6 +1,7 @@
using Dapper; using Dapper;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
@ -13,6 +14,7 @@ namespace ErsatzTV.Application.Libraries;
public class MoveLocalLibraryPathHandler : IRequestHandler<MoveLocalLibraryPath, Either<BaseError, Unit>> public class MoveLocalLibraryPathHandler : IRequestHandler<MoveLocalLibraryPath, Either<BaseError, Unit>>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILogger<MoveLocalLibraryPathHandler> _logger; private readonly ILogger<MoveLocalLibraryPathHandler> _logger;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository; private readonly ISearchRepository _searchRepository;
@ -20,11 +22,13 @@ public class MoveLocalLibraryPathHandler : IRequestHandler<MoveLocalLibraryPath,
public MoveLocalLibraryPathHandler( public MoveLocalLibraryPathHandler(
ISearchIndex searchIndex, ISearchIndex searchIndex,
ISearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
ILogger<MoveLocalLibraryPathHandler> logger) ILogger<MoveLocalLibraryPathHandler> logger)
{ {
_searchIndex = searchIndex; _searchIndex = searchIndex;
_searchRepository = searchRepository; _searchRepository = searchRepository;
_fallbackMetadataProvider = fallbackMetadataProvider;
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_logger = logger; _logger = logger;
} }
@ -35,7 +39,7 @@ public class MoveLocalLibraryPathHandler : IRequestHandler<MoveLocalLibraryPath,
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Parameters> validation = await Validate(dbContext, request); Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, parameters => MovePath(dbContext, parameters)); return await validation.Apply(parameters => MovePath(dbContext, parameters));
} }
private async Task<Unit> MovePath(TvContext dbContext, Parameters parameters) private async Task<Unit> MovePath(TvContext dbContext, Parameters parameters)
@ -57,7 +61,10 @@ public class MoveLocalLibraryPathHandler : IRequestHandler<MoveLocalLibraryPath,
foreach (MediaItem mediaItem in maybeMediaItem) foreach (MediaItem mediaItem in maybeMediaItem)
{ {
_logger.LogInformation("Moving item at {Path}", await GetPath(dbContext, mediaItem)); _logger.LogInformation("Moving item at {Path}", await GetPath(dbContext, mediaItem));
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { mediaItem }); await _searchIndex.UpdateItems(
_searchRepository,
_fallbackMetadataProvider,
new List<MediaItem> { mediaItem });
} }
} }
} }

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

@ -1,6 +1,7 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Core.Interfaces.Trakt;
@ -19,10 +20,11 @@ public class AddTraktListHandler : TraktCommandBase, IRequestHandler<AddTraktLis
ITraktApiClient traktApiClient, ITraktApiClient traktApiClient,
ISearchRepository searchRepository, ISearchRepository searchRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider,
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
ILogger<AddTraktListHandler> logger, ILogger<AddTraktListHandler> logger,
IEntityLocker entityLocker) IEntityLocker entityLocker)
: base(traktApiClient, searchRepository, searchIndex, logger) : base(traktApiClient, searchRepository, searchIndex, fallbackMetadataProvider, logger)
{ {
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_entityLocker = entityLocker; _entityLocker = entityLocker;

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

@ -1,6 +1,7 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Core.Interfaces.Trakt;
@ -14,6 +15,7 @@ public class DeleteTraktListHandler : TraktCommandBase, IRequestHandler<DeleteTr
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEntityLocker _entityLocker; private readonly IEntityLocker _entityLocker;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository; private readonly ISearchRepository _searchRepository;
@ -21,13 +23,15 @@ public class DeleteTraktListHandler : TraktCommandBase, IRequestHandler<DeleteTr
ITraktApiClient traktApiClient, ITraktApiClient traktApiClient,
ISearchRepository searchRepository, ISearchRepository searchRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider,
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
ILogger<DeleteTraktListHandler> logger, ILogger<DeleteTraktListHandler> logger,
IEntityLocker entityLocker) IEntityLocker entityLocker)
: base(traktApiClient, searchRepository, searchIndex, logger) : base(traktApiClient, searchRepository, searchIndex, fallbackMetadataProvider, logger)
{ {
_searchRepository = searchRepository; _searchRepository = searchRepository;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_fallbackMetadataProvider = fallbackMetadataProvider;
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_entityLocker = entityLocker; _entityLocker = entityLocker;
} }
@ -38,8 +42,7 @@ public class DeleteTraktListHandler : TraktCommandBase, IRequestHandler<DeleteTr
{ {
try try
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, TraktList> validation = await TraktListMustExist(dbContext, request.TraktListId); Validation<BaseError, TraktList> validation = await TraktListMustExist(dbContext, request.TraktListId);
return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c)); return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c));
} }
@ -56,7 +59,7 @@ public class DeleteTraktListHandler : TraktCommandBase, IRequestHandler<DeleteTr
dbContext.TraktLists.Remove(traktList); dbContext.TraktLists.Remove(traktList);
if (await dbContext.SaveChangesAsync() > 0) if (await dbContext.SaveChangesAsync() > 0)
{ {
await _searchIndex.RebuildItems(_searchRepository, mediaItemIds); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, mediaItemIds);
} }
_searchIndex.Commit(); _searchIndex.Commit();

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

@ -1,6 +1,7 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Core.Interfaces.Trakt;
@ -20,9 +21,15 @@ public class MatchTraktListItemsHandler : TraktCommandBase,
ITraktApiClient traktApiClient, ITraktApiClient traktApiClient,
ISearchRepository searchRepository, ISearchRepository searchRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider,
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
ILogger<MatchTraktListItemsHandler> logger, ILogger<MatchTraktListItemsHandler> logger,
IEntityLocker entityLocker) : base(traktApiClient, searchRepository, searchIndex, logger) IEntityLocker entityLocker) : base(
traktApiClient,
searchRepository,
searchIndex,
fallbackMetadataProvider,
logger)
{ {
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_entityLocker = entityLocker; _entityLocker = entityLocker;

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

@ -1,5 +1,6 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Core.Interfaces.Trakt;
@ -13,6 +14,7 @@ namespace ErsatzTV.Application.MediaCollections;
public abstract class TraktCommandBase public abstract class TraktCommandBase
{ {
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository; private readonly ISearchRepository _searchRepository;
@ -21,10 +23,12 @@ public abstract class TraktCommandBase
ITraktApiClient traktApiClient, ITraktApiClient traktApiClient,
ISearchRepository searchRepository, ISearchRepository searchRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider,
ILogger logger) ILogger logger)
{ {
_searchRepository = searchRepository; _searchRepository = searchRepository;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_fallbackMetadataProvider = fallbackMetadataProvider;
_logger = logger; _logger = logger;
TraktApiClient = traktApiClient; TraktApiClient = traktApiClient;
} }
@ -158,7 +162,7 @@ public abstract class TraktCommandBase
if (await dbContext.SaveChangesAsync() > 0) if (await dbContext.SaveChangesAsync() > 0)
{ {
await _searchIndex.RebuildItems(_searchRepository, ids.ToList()); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, ids.ToList());
} }
_searchIndex.Commit(); _searchIndex.Commit();

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

@ -12,6 +12,7 @@ namespace ErsatzTV.Application.Search;
public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex, Unit> public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex, Unit>
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<RebuildSearchIndexHandler> _logger; private readonly ILogger<RebuildSearchIndexHandler> _logger;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
@ -22,6 +23,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex, Uni
ISearchRepository searchRepository, ISearchRepository searchRepository,
IConfigElementRepository configElementRepository, IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
IFallbackMetadataProvider fallbackMetadataProvider,
ILogger<RebuildSearchIndexHandler> logger) ILogger<RebuildSearchIndexHandler> logger)
{ {
_searchIndex = searchIndex; _searchIndex = searchIndex;
@ -29,6 +31,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex, Uni
_searchRepository = searchRepository; _searchRepository = searchRepository;
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_fallbackMetadataProvider = fallbackMetadataProvider;
} }
public async Task<Unit> Handle(RebuildSearchIndex request, CancellationToken cancellationToken) public async Task<Unit> Handle(RebuildSearchIndex request, CancellationToken cancellationToken)
@ -48,7 +51,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex, Uni
_logger.LogInformation("Migrating search index to version {Version}", _searchIndex.Version); _logger.LogInformation("Migrating search index to version {Version}", _searchIndex.Version);
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
await _searchIndex.Rebuild(_searchRepository); await _searchIndex.Rebuild(_searchRepository, _fallbackMetadataProvider);
await _configElementRepository.Upsert(ConfigElementKey.SearchIndexVersion, _searchIndex.Version); await _configElementRepository.Upsert(ConfigElementKey.SearchIndexVersion, _searchIndex.Version);
sw.Stop(); sw.Stop();

48
ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs

@ -3,10 +3,14 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Search; using ErsatzTV.Core.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCards.Mapper; using static ErsatzTV.Application.MediaCards.Mapper;
namespace ErsatzTV.Application.Search; namespace ErsatzTV.Application.Search;
@ -14,7 +18,9 @@ namespace ErsatzTV.Application.Search;
public class public class
QuerySearchIndexEpisodesHandler : IRequestHandler<QuerySearchIndexEpisodes, TelevisionEpisodeCardResultsViewModel> QuerySearchIndexEpisodesHandler : IRequestHandler<QuerySearchIndexEpisodes, TelevisionEpisodeCardResultsViewModel>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEmbyPathReplacementService _embyPathReplacementService; private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService; private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IPlexPathReplacementService _plexPathReplacementService; private readonly IPlexPathReplacementService _plexPathReplacementService;
@ -27,7 +33,9 @@ public class
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
IPlexPathReplacementService plexPathReplacementService, IPlexPathReplacementService plexPathReplacementService,
IJellyfinPathReplacementService jellyfinPathReplacementService, IJellyfinPathReplacementService jellyfinPathReplacementService,
IEmbyPathReplacementService embyPathReplacementService) IEmbyPathReplacementService embyPathReplacementService,
IFallbackMetadataProvider fallbackMetadataProvider,
IDbContextFactory<TvContext> dbContextFactory)
{ {
_searchIndex = searchIndex; _searchIndex = searchIndex;
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
@ -35,6 +43,8 @@ public class
_plexPathReplacementService = plexPathReplacementService; _plexPathReplacementService = plexPathReplacementService;
_jellyfinPathReplacementService = jellyfinPathReplacementService; _jellyfinPathReplacementService = jellyfinPathReplacementService;
_embyPathReplacementService = embyPathReplacementService; _embyPathReplacementService = embyPathReplacementService;
_fallbackMetadataProvider = fallbackMetadataProvider;
_dbContextFactory = dbContextFactory;
} }
public async Task<TelevisionEpisodeCardResultsViewModel> Handle( public async Task<TelevisionEpisodeCardResultsViewModel> Handle(
@ -52,8 +62,40 @@ public class
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby() Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby()
.Map(list => list.HeadOrNone()); .Map(list => list.HeadOrNone());
List<EpisodeMetadata> episodes = await _televisionRepository var episodeIds = searchResult.Items.Map(i => i.Id).ToList();
.GetEpisodesForCards(searchResult.Items.Map(i => i.Id).ToList());
List<EpisodeMetadata> episodes = await _televisionRepository.GetEpisodesForCards(episodeIds);
// try to load fallback metadata for episodes that have none
// this handles an edge case of trashed items with no saved metadata
var missingEpisodes = episodeIds.Except(episodes.Map(e => e.EpisodeId)).ToList();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
foreach (int missingEpisodeId in missingEpisodes)
{
Option<Episode> maybeEpisode = await dbContext.Episodes
.AsNoTracking()
.Include(e => e.MediaVersions)
.ThenInclude(e => e.MediaFiles)
.Include(e => e.Season)
.ThenInclude(s => s.SeasonMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(e => e.Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.SelectOneAsync(e => e.Id, e => e.Id == missingEpisodeId);
foreach (Episode episode in maybeEpisode)
{
foreach (EpisodeMetadata headMetadata in _fallbackMetadataProvider.GetFallbackMetadata(episode)
.HeadOrNone())
{
headMetadata.Episode = episode;
episode.EpisodeMetadata = new List<EpisodeMetadata> { headMetadata };
episodes.Add(headMetadata);
}
}
}
var items = new List<TelevisionEpisodeCardViewModel>(); var items = new List<TelevisionEpisodeCardViewModel>();

7
ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs

@ -40,6 +40,13 @@ public class FallbackMetadataProviderTests
"Awesome.Show.S01E02.Description.more.Description.QUAlity.codec.CODEC-GROUP.mkv", "Awesome.Show.S01E02.Description.more.Description.QUAlity.codec.CODEC-GROUP.mkv",
1, 1,
2)] 2)]
[TestCase("Awesome Show - s01.e02.mkv", 1, 2)]
[TestCase("Awesome Show - S01.E02.mkv", 1, 2)]
[TestCase("Awesome Show - s01_e02.mkv", 1, 2)]
[TestCase("Awesome Show - S01_E02.mkv", 1, 2)]
[TestCase("Awesome Show - s01xe02.mkv", 1, 2)]
[TestCase("Awesome Show - S01XE02.mkv", 1, 2)]
[TestCase("Awesome Show - 1x02.mkv", 1, 2)]
public void GetFallbackMetadata_ShouldHandleVariousFormats(string path, int season, int episode) public void GetFallbackMetadata_ShouldHandleVariousFormats(string path, int season, int episode)
{ {
List<EpisodeMetadata> metadata = _fallbackMetadataProvider.GetFallbackMetadata( List<EpisodeMetadata> metadata = _fallbackMetadataProvider.GetFallbackMetadata(

2
ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs

@ -623,6 +623,7 @@ public class MovieFolderScannerTests
_imageCache.Object, _imageCache.Object,
new Mock<ISearchIndex>().Object, new Mock<ISearchIndex>().Object,
new Mock<ISearchRepository>().Object, new Mock<ISearchRepository>().Object,
new Mock<IFallbackMetadataProvider>().Object,
new Mock<ILibraryRepository>().Object, new Mock<ILibraryRepository>().Object,
_mediaItemRepository.Object, _mediaItemRepository.Object,
new Mock<IMediator>().Object, new Mock<IMediator>().Object,
@ -643,6 +644,7 @@ public class MovieFolderScannerTests
_imageCache.Object, _imageCache.Object,
new Mock<ISearchIndex>().Object, new Mock<ISearchIndex>().Object,
new Mock<ISearchRepository>().Object, new Mock<ISearchRepository>().Object,
new Mock<IFallbackMetadataProvider>().Object,
new Mock<ILibraryRepository>().Object, new Mock<ILibraryRepository>().Object,
_mediaItemRepository.Object, _mediaItemRepository.Object,
new Mock<IMediator>().Object, new Mock<IMediator>().Object,

6
ErsatzTV.Core/Emby/EmbyCollectionScanner.cs

@ -1,5 +1,6 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -10,6 +11,7 @@ public class EmbyCollectionScanner : IEmbyCollectionScanner
{ {
private readonly IEmbyApiClient _embyApiClient; private readonly IEmbyApiClient _embyApiClient;
private readonly IEmbyCollectionRepository _embyCollectionRepository; private readonly IEmbyCollectionRepository _embyCollectionRepository;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILogger<EmbyCollectionScanner> _logger; private readonly ILogger<EmbyCollectionScanner> _logger;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository; private readonly ISearchRepository _searchRepository;
@ -19,12 +21,14 @@ public class EmbyCollectionScanner : IEmbyCollectionScanner
IEmbyApiClient embyApiClient, IEmbyApiClient embyApiClient,
ISearchRepository searchRepository, ISearchRepository searchRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider,
ILogger<EmbyCollectionScanner> logger) ILogger<EmbyCollectionScanner> logger)
{ {
_embyCollectionRepository = embyCollectionRepository; _embyCollectionRepository = embyCollectionRepository;
_embyApiClient = embyApiClient; _embyApiClient = embyApiClient;
_searchRepository = searchRepository; _searchRepository = searchRepository;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_fallbackMetadataProvider = fallbackMetadataProvider;
_logger = logger; _logger = logger;
} }
@ -107,7 +111,7 @@ public class EmbyCollectionScanner : IEmbyCollectionScanner
var changedIds = removedIds.Except(addedIds).ToList(); var changedIds = removedIds.Except(addedIds).ToList();
changedIds.AddRange(addedIds.Except(removedIds)); changedIds.AddRange(addedIds.Except(removedIds));
await _searchIndex.RebuildItems(_searchRepository, changedIds); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, changedIds);
_searchIndex.Commit(); _searchIndex.Commit();
} }
catch (Exception ex) catch (Exception ex)

2
ErsatzTV.Core/Emby/EmbyMovieLibraryScanner.cs

@ -26,6 +26,7 @@ public class EmbyMovieLibraryScanner :
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
IEmbyMovieRepository embyMovieRepository, IEmbyMovieRepository embyMovieRepository,
ISearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IEmbyPathReplacementService pathReplacementService, IEmbyPathReplacementService pathReplacementService,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider, ILocalStatisticsProvider localStatisticsProvider,
@ -38,6 +39,7 @@ public class EmbyMovieLibraryScanner :
mediator, mediator,
searchIndex, searchIndex,
searchRepository, searchRepository,
fallbackMetadataProvider,
logger) logger)
{ {
_embyApiClient = embyApiClient; _embyApiClient = embyApiClient;

2
ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs

@ -25,6 +25,7 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
IEmbyTelevisionRepository televisionRepository, IEmbyTelevisionRepository televisionRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
ISearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IEmbyPathReplacementService pathReplacementService, IEmbyPathReplacementService pathReplacementService,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider, ILocalStatisticsProvider localStatisticsProvider,
@ -37,6 +38,7 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
localFileSystem, localFileSystem,
searchRepository, searchRepository,
searchIndex, searchIndex,
fallbackMetadataProvider,
mediator, mediator,
logger) logger)
{ {

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

@ -9,9 +9,18 @@ public interface ISearchIndex : IDisposable
{ {
public int Version { get; } public int Version { get; }
Task<bool> Initialize(ILocalFileSystem localFileSystem, IConfigElementRepository configElementRepository); Task<bool> Initialize(ILocalFileSystem localFileSystem, IConfigElementRepository configElementRepository);
Task<Unit> Rebuild(ISearchRepository searchRepository); Task<Unit> Rebuild(ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider);
Task<Unit> RebuildItems(ISearchRepository searchRepository, List<int> itemIds);
Task<Unit> UpdateItems(ISearchRepository searchRepository, List<MediaItem> items); Task<Unit> RebuildItems(
ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
List<int> itemIds);
Task<Unit> UpdateItems(
ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
List<MediaItem> items);
Task<Unit> RemoveItems(List<int> ids); Task<Unit> RemoveItems(List<int> ids);
Task<SearchResult> Search(string query, int skip, int limit, string searchField = ""); Task<SearchResult> Search(string query, int skip, int limit, string searchField = "");
void Commit(); void Commit();

6
ErsatzTV.Core/Jellyfin/JellyfinCollectionScanner.cs

@ -1,5 +1,6 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -8,6 +9,7 @@ namespace ErsatzTV.Core.Jellyfin;
public class JellyfinCollectionScanner : IJellyfinCollectionScanner public class JellyfinCollectionScanner : IJellyfinCollectionScanner
{ {
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly IJellyfinApiClient _jellyfinApiClient; private readonly IJellyfinApiClient _jellyfinApiClient;
private readonly IJellyfinCollectionRepository _jellyfinCollectionRepository; private readonly IJellyfinCollectionRepository _jellyfinCollectionRepository;
private readonly ILogger<JellyfinCollectionScanner> _logger; private readonly ILogger<JellyfinCollectionScanner> _logger;
@ -19,12 +21,14 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner
IJellyfinApiClient jellyfinApiClient, IJellyfinApiClient jellyfinApiClient,
ISearchRepository searchRepository, ISearchRepository searchRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider,
ILogger<JellyfinCollectionScanner> logger) ILogger<JellyfinCollectionScanner> logger)
{ {
_jellyfinCollectionRepository = jellyfinCollectionRepository; _jellyfinCollectionRepository = jellyfinCollectionRepository;
_jellyfinApiClient = jellyfinApiClient; _jellyfinApiClient = jellyfinApiClient;
_searchRepository = searchRepository; _searchRepository = searchRepository;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_fallbackMetadataProvider = fallbackMetadataProvider;
_logger = logger; _logger = logger;
} }
@ -116,7 +120,7 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner
var changedIds = removedIds.Except(addedIds).ToList(); var changedIds = removedIds.Except(addedIds).ToList();
changedIds.AddRange(addedIds.Except(removedIds)); changedIds.AddRange(addedIds.Except(removedIds));
await _searchIndex.RebuildItems(_searchRepository, changedIds); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, changedIds);
_searchIndex.Commit(); _searchIndex.Commit();
} }
catch (Exception ex) catch (Exception ex)

2
ErsatzTV.Core/Jellyfin/JellyfinMovieLibraryScanner.cs

@ -25,6 +25,7 @@ public class JellyfinMovieLibraryScanner :
IMediator mediator, IMediator mediator,
IJellyfinMovieRepository jellyfinMovieRepository, IJellyfinMovieRepository jellyfinMovieRepository,
ISearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IJellyfinPathReplacementService pathReplacementService, IJellyfinPathReplacementService pathReplacementService,
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
@ -38,6 +39,7 @@ public class JellyfinMovieLibraryScanner :
mediator, mediator,
searchIndex, searchIndex,
searchRepository, searchRepository,
fallbackMetadataProvider,
logger) logger)
{ {
_jellyfinApiClient = jellyfinApiClient; _jellyfinApiClient = jellyfinApiClient;

2
ErsatzTV.Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs

@ -26,6 +26,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
IJellyfinTelevisionRepository televisionRepository, IJellyfinTelevisionRepository televisionRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
ISearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IJellyfinPathReplacementService pathReplacementService, IJellyfinPathReplacementService pathReplacementService,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider, ILocalStatisticsProvider localStatisticsProvider,
@ -38,6 +39,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
localFileSystem, localFileSystem,
searchRepository, searchRepository,
searchIndex, searchIndex,
fallbackMetadataProvider,
mediator, mediator,
logger) logger)
{ {

8
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -161,8 +161,14 @@ public class FallbackMetadataProvider : IFallbackMetadataProvider
try try
{ {
const string PATTERN = @"[sS]\d+[eE]([e\-\d{1,2}]+)"; const string PATTERN = @"[sS]\d+[\._xX]?[eE]([e\-\d{1,2}]+)";
const string PATTERN_2 = @"\d+[\._xX]([e\-\d{1,2}]+)";
MatchCollection matches = Regex.Matches(fileName, PATTERN); MatchCollection matches = Regex.Matches(fileName, PATTERN);
if (matches.Count == 0)
{
matches = Regex.Matches(fileName, PATTERN_2);
}
if (matches.Count > 0) if (matches.Count > 0)
{ {
foreach (Match match in matches) foreach (Match match in matches)

12
ErsatzTV.Core/Metadata/MediaServerMovieLibraryScanner.cs

@ -15,6 +15,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
where TMovie : Movie where TMovie : Movie
where TEtag : MediaServerItemEtag where TEtag : MediaServerItemEtag
{ {
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider; private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider; private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
@ -30,6 +31,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
IMediator mediator, IMediator mediator,
ISearchIndex searchIndex, ISearchIndex searchIndex,
ISearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
ILogger logger) ILogger logger)
{ {
_localStatisticsProvider = localStatisticsProvider; _localStatisticsProvider = localStatisticsProvider;
@ -38,6 +40,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
_mediator = mediator; _mediator = mediator;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_searchRepository = searchRepository; _searchRepository = searchRepository;
_fallbackMetadataProvider = fallbackMetadataProvider;
_logger = logger; _logger = logger;
} }
@ -168,7 +171,10 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
if (result.IsAdded || result.IsUpdated) if (result.IsAdded || result.IsUpdated)
{ {
await _searchIndex.RebuildItems(_searchRepository, new List<int> { result.Item.Id }); await _searchIndex.RebuildItems(
_searchRepository,
_fallbackMetadataProvider,
new List<int> { result.Item.Id });
} }
} }
} }
@ -176,7 +182,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
// trash movies that are no longer present on the media server // trash movies that are no longer present on the media server
var fileNotFoundItemIds = existingMovies.Map(m => m.MediaServerItemId).Except(incomingItemIds).ToList(); var fileNotFoundItemIds = existingMovies.Map(m => m.MediaServerItemId).Except(incomingItemIds).ToList();
List<int> ids = await movieRepository.FlagFileNotFound(library, fileNotFoundItemIds); List<int> ids = await movieRepository.FlagFileNotFound(library, fileNotFoundItemIds);
await _searchIndex.RebuildItems(_searchRepository, ids); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, ids);
await _mediator.Publish(new LibraryScanProgress(library.Id, 0), cancellationToken); await _mediator.Publish(new LibraryScanProgress(library.Id, 0), cancellationToken);
@ -241,7 +247,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
{ {
foreach (int id in await movieRepository.FlagUnavailable(library, incoming)) foreach (int id in await movieRepository.FlagUnavailable(library, incoming))
{ {
await _searchIndex.RebuildItems(_searchRepository, new List<int> { id }); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, new List<int> { id });
} }
} }

26
ErsatzTV.Core/Metadata/MediaServerTelevisionLibraryScanner.cs

@ -18,6 +18,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
where TEpisode : Episode where TEpisode : Episode
where TEtag : MediaServerItemEtag where TEtag : MediaServerItemEtag
{ {
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider; private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider; private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
@ -32,6 +33,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
ISearchRepository searchRepository, ISearchRepository searchRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider,
IMediator mediator, IMediator mediator,
ILogger logger) ILogger logger)
{ {
@ -40,6 +42,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_searchRepository = searchRepository; _searchRepository = searchRepository;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_fallbackMetadataProvider = fallbackMetadataProvider;
_mediator = mediator; _mediator = mediator;
_logger = logger; _logger = logger;
} }
@ -196,7 +199,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
if (result.IsAdded || result.IsUpdated) if (result.IsAdded || result.IsUpdated)
{ {
await _searchIndex.RebuildItems(_searchRepository, new List<int> { result.Item.Id }); await _searchIndex.RebuildItems(
_searchRepository,
_fallbackMetadataProvider,
new List<int> { result.Item.Id });
} }
} }
} }
@ -204,7 +210,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
// trash shows that are no longer present on the media server // trash shows that are no longer present on the media server
var fileNotFoundItemIds = existingShows.Map(s => s.MediaServerItemId).Except(incomingItemIds).ToList(); var fileNotFoundItemIds = existingShows.Map(s => s.MediaServerItemId).Except(incomingItemIds).ToList();
List<int> ids = await televisionRepository.FlagFileNotFoundShows(library, fileNotFoundItemIds); List<int> ids = await televisionRepository.FlagFileNotFoundShows(library, fileNotFoundItemIds);
await _searchIndex.RebuildItems(_searchRepository, ids); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, ids);
await _mediator.Publish(new LibraryScanProgress(library.Id, 0), cancellationToken); await _mediator.Publish(new LibraryScanProgress(library.Id, 0), cancellationToken);
@ -358,7 +364,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
if (result.IsAdded || result.IsUpdated) if (result.IsAdded || result.IsUpdated)
{ {
await _searchIndex.RebuildItems(_searchRepository, new List<int> { result.Item.Id }); await _searchIndex.RebuildItems(
_searchRepository,
_fallbackMetadataProvider,
new List<int> { result.Item.Id });
} }
} }
} }
@ -366,7 +375,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
// trash seasons that are no longer present on the media server // trash seasons that are no longer present on the media server
var fileNotFoundItemIds = existingSeasons.Map(s => s.MediaServerItemId).Except(incomingItemIds).ToList(); var fileNotFoundItemIds = existingSeasons.Map(s => s.MediaServerItemId).Except(incomingItemIds).ToList();
List<int> ids = await televisionRepository.FlagFileNotFoundSeasons(library, fileNotFoundItemIds); List<int> ids = await televisionRepository.FlagFileNotFoundSeasons(library, fileNotFoundItemIds);
await _searchIndex.RebuildItems(_searchRepository, ids); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, ids);
return Unit.Default; return Unit.Default;
} }
@ -461,7 +470,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
if (result.IsAdded || result.IsUpdated) if (result.IsAdded || result.IsUpdated)
{ {
await _searchIndex.RebuildItems(_searchRepository, new List<int> { result.Item.Id }); await _searchIndex.RebuildItems(
_searchRepository,
_fallbackMetadataProvider,
new List<int> { result.Item.Id });
} }
} }
} }
@ -469,7 +481,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
// trash episodes that are no longer present on the media server // trash episodes that are no longer present on the media server
var fileNotFoundItemIds = existingEpisodes.Map(m => m.MediaServerItemId).Except(incomingItemIds).ToList(); var fileNotFoundItemIds = existingEpisodes.Map(m => m.MediaServerItemId).Except(incomingItemIds).ToList();
List<int> ids = await televisionRepository.FlagFileNotFoundEpisodes(library, fileNotFoundItemIds); List<int> ids = await televisionRepository.FlagFileNotFoundEpisodes(library, fileNotFoundItemIds);
await _searchIndex.RebuildItems(_searchRepository, ids); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, ids);
return Unit.Default; return Unit.Default;
} }
@ -511,7 +523,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
{ {
foreach (int id in await televisionRepository.FlagUnavailable(library, incoming)) foreach (int id in await televisionRepository.FlagUnavailable(library, incoming))
{ {
await _searchIndex.RebuildItems(_searchRepository, new List<int> { id }); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, new List<int> { id });
} }
} }

10
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -15,6 +15,7 @@ namespace ErsatzTV.Core.Metadata;
public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
{ {
private readonly IClient _client; private readonly IClient _client;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILibraryRepository _libraryRepository; private readonly ILibraryRepository _libraryRepository;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILocalMetadataProvider _localMetadataProvider;
@ -35,6 +36,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
IImageCache imageCache, IImageCache imageCache,
ISearchIndex searchIndex, ISearchIndex searchIndex,
ISearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
ILibraryRepository libraryRepository, ILibraryRepository libraryRepository,
IMediaItemRepository mediaItemRepository, IMediaItemRepository mediaItemRepository,
IMediator mediator, IMediator mediator,
@ -59,6 +61,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
_localMetadataProvider = localMetadataProvider; _localMetadataProvider = localMetadataProvider;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_searchRepository = searchRepository; _searchRepository = searchRepository;
_fallbackMetadataProvider = fallbackMetadataProvider;
_libraryRepository = libraryRepository; _libraryRepository = libraryRepository;
_mediator = mediator; _mediator = mediator;
_client = client; _client = client;
@ -160,7 +163,10 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
{ {
if (result.IsAdded || result.IsUpdated) if (result.IsAdded || result.IsUpdated)
{ {
await _searchIndex.RebuildItems(_searchRepository, new List<int> { result.Item.Id }); await _searchIndex.RebuildItems(
_searchRepository,
_fallbackMetadataProvider,
new List<int> { result.Item.Id });
} }
await _libraryRepository.SetEtag(libraryPath, knownFolder, movieFolder, etag); await _libraryRepository.SetEtag(libraryPath, knownFolder, movieFolder, etag);
@ -174,7 +180,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
{ {
_logger.LogInformation("Flagging missing movie at {Path}", path); _logger.LogInformation("Flagging missing movie at {Path}", path);
List<int> ids = await FlagFileNotFound(libraryPath, path); List<int> ids = await FlagFileNotFound(libraryPath, path);
await _searchIndex.RebuildItems(_searchRepository, ids); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, ids);
} }
else if (Path.GetFileName(path).StartsWith("._")) else if (Path.GetFileName(path).StartsWith("._"))
{ {

15
ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs

@ -15,6 +15,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
{ {
private readonly IArtistRepository _artistRepository; private readonly IArtistRepository _artistRepository;
private readonly IClient _client; private readonly IClient _client;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILibraryRepository _libraryRepository; private readonly ILibraryRepository _libraryRepository;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILocalMetadataProvider _localMetadataProvider;
@ -34,6 +35,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
IImageCache imageCache, IImageCache imageCache,
ISearchIndex searchIndex, ISearchIndex searchIndex,
ISearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IArtistRepository artistRepository, IArtistRepository artistRepository,
IMusicVideoRepository musicVideoRepository, IMusicVideoRepository musicVideoRepository,
ILibraryRepository libraryRepository, ILibraryRepository libraryRepository,
@ -58,6 +60,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
_localSubtitlesProvider = localSubtitlesProvider; _localSubtitlesProvider = localSubtitlesProvider;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_searchRepository = searchRepository; _searchRepository = searchRepository;
_fallbackMetadataProvider = fallbackMetadataProvider;
_artistRepository = artistRepository; _artistRepository = artistRepository;
_musicVideoRepository = musicVideoRepository; _musicVideoRepository = musicVideoRepository;
_libraryRepository = libraryRepository; _libraryRepository = libraryRepository;
@ -137,7 +140,10 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
if (result.IsAdded || result.IsUpdated) if (result.IsAdded || result.IsUpdated)
{ {
await _searchIndex.RebuildItems(_searchRepository, new List<int> { result.Item.Id }); await _searchIndex.RebuildItems(
_searchRepository,
_fallbackMetadataProvider,
new List<int> { result.Item.Id });
} }
} }
} }
@ -155,7 +161,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
{ {
_logger.LogInformation("Flagging missing music video at {Path}", path); _logger.LogInformation("Flagging missing music video at {Path}", path);
List<int> musicVideoIds = await FlagFileNotFound(libraryPath, path); List<int> musicVideoIds = await FlagFileNotFound(libraryPath, path);
await _searchIndex.RebuildItems(_searchRepository, musicVideoIds); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, musicVideoIds);
} }
else if (Path.GetFileName(path).StartsWith("._")) else if (Path.GetFileName(path).StartsWith("._"))
{ {
@ -330,7 +336,10 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
{ {
if (result.IsAdded || result.IsUpdated) if (result.IsAdded || result.IsUpdated)
{ {
await _searchIndex.RebuildItems(_searchRepository, new List<int> { result.Item.Id }); await _searchIndex.RebuildItems(
_searchRepository,
_fallbackMetadataProvider,
new List<int> { result.Item.Id });
} }
} }
} }

10
ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs

@ -14,6 +14,7 @@ namespace ErsatzTV.Core.Metadata;
public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScanner public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScanner
{ {
private readonly IClient _client; private readonly IClient _client;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILibraryRepository _libraryRepository; private readonly ILibraryRepository _libraryRepository;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILocalMetadataProvider _localMetadataProvider;
@ -34,6 +35,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
IMediator mediator, IMediator mediator,
ISearchIndex searchIndex, ISearchIndex searchIndex,
ISearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IOtherVideoRepository otherVideoRepository, IOtherVideoRepository otherVideoRepository,
ILibraryRepository libraryRepository, ILibraryRepository libraryRepository,
IMediaItemRepository mediaItemRepository, IMediaItemRepository mediaItemRepository,
@ -57,6 +59,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
_mediator = mediator; _mediator = mediator;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_searchRepository = searchRepository; _searchRepository = searchRepository;
_fallbackMetadataProvider = fallbackMetadataProvider;
_otherVideoRepository = otherVideoRepository; _otherVideoRepository = otherVideoRepository;
_libraryRepository = libraryRepository; _libraryRepository = libraryRepository;
_client = client; _client = client;
@ -160,7 +163,10 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
{ {
if (result.IsAdded || result.IsUpdated) if (result.IsAdded || result.IsUpdated)
{ {
await _searchIndex.RebuildItems(_searchRepository, new List<int> { result.Item.Id }); await _searchIndex.RebuildItems(
_searchRepository,
_fallbackMetadataProvider,
new List<int> { result.Item.Id });
} }
} }
} }
@ -178,7 +184,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
{ {
_logger.LogInformation("Flagging missing other video at {Path}", path); _logger.LogInformation("Flagging missing other video at {Path}", path);
List<int> otherVideoIds = await FlagFileNotFound(libraryPath, path); List<int> otherVideoIds = await FlagFileNotFound(libraryPath, path);
await _searchIndex.RebuildItems(_searchRepository, otherVideoIds); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, otherVideoIds);
} }
else if (Path.GetFileName(path).StartsWith("._")) else if (Path.GetFileName(path).StartsWith("._"))
{ {

10
ErsatzTV.Core/Metadata/SongFolderScanner.cs

@ -15,6 +15,7 @@ namespace ErsatzTV.Core.Metadata;
public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
{ {
private readonly IClient _client; private readonly IClient _client;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILibraryRepository _libraryRepository; private readonly ILibraryRepository _libraryRepository;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILocalMetadataProvider _localMetadataProvider;
@ -33,6 +34,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
IMediator mediator, IMediator mediator,
ISearchIndex searchIndex, ISearchIndex searchIndex,
ISearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
ISongRepository songRepository, ISongRepository songRepository,
ILibraryRepository libraryRepository, ILibraryRepository libraryRepository,
IMediaItemRepository mediaItemRepository, IMediaItemRepository mediaItemRepository,
@ -55,6 +57,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
_mediator = mediator; _mediator = mediator;
_searchIndex = searchIndex; _searchIndex = searchIndex;
_searchRepository = searchRepository; _searchRepository = searchRepository;
_fallbackMetadataProvider = fallbackMetadataProvider;
_songRepository = songRepository; _songRepository = songRepository;
_libraryRepository = libraryRepository; _libraryRepository = libraryRepository;
_client = client; _client = client;
@ -155,7 +158,10 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
{ {
if (result.IsAdded || result.IsUpdated) if (result.IsAdded || result.IsUpdated)
{ {
await _searchIndex.RebuildItems(_searchRepository, new List<int> { result.Item.Id }); await _searchIndex.RebuildItems(
_searchRepository,
_fallbackMetadataProvider,
new List<int> { result.Item.Id });
} }
} }
} }
@ -173,7 +179,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
{ {
_logger.LogInformation("Flagging missing song at {Path}", path); _logger.LogInformation("Flagging missing song at {Path}", path);
List<int> songIds = await FlagFileNotFound(libraryPath, path); List<int> songIds = await FlagFileNotFound(libraryPath, path);
await _searchIndex.RebuildItems(_searchRepository, songIds); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, songIds);
} }
else if (Path.GetFileName(path).StartsWith("._")) else if (Path.GetFileName(path).StartsWith("._"))
{ {

21
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -14,6 +14,7 @@ namespace ErsatzTV.Core.Metadata;
public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScanner public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScanner
{ {
private readonly IClient _client; private readonly IClient _client;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILibraryRepository _libraryRepository; private readonly ILibraryRepository _libraryRepository;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILocalMetadataProvider _localMetadataProvider;
@ -30,6 +31,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
ITelevisionRepository televisionRepository, ITelevisionRepository televisionRepository,
ILocalStatisticsProvider localStatisticsProvider, ILocalStatisticsProvider localStatisticsProvider,
ILocalMetadataProvider localMetadataProvider, ILocalMetadataProvider localMetadataProvider,
IFallbackMetadataProvider fallbackMetadataProvider,
ILocalSubtitlesProvider localSubtitlesProvider, ILocalSubtitlesProvider localSubtitlesProvider,
IMetadataRepository metadataRepository, IMetadataRepository metadataRepository,
IImageCache imageCache, IImageCache imageCache,
@ -55,6 +57,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
_localMetadataProvider = localMetadataProvider; _localMetadataProvider = localMetadataProvider;
_fallbackMetadataProvider = fallbackMetadataProvider;
_localSubtitlesProvider = localSubtitlesProvider; _localSubtitlesProvider = localSubtitlesProvider;
_metadataRepository = metadataRepository; _metadataRepository = metadataRepository;
_searchIndex = searchIndex; _searchIndex = searchIndex;
@ -127,7 +130,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
if (result.IsAdded || result.IsUpdated) if (result.IsAdded || result.IsUpdated)
{ {
await _searchIndex.RebuildItems(_searchRepository, new List<int> { result.Item.Id }); await _searchIndex.RebuildItems(
_searchRepository,
_fallbackMetadataProvider,
new List<int> { result.Item.Id });
} }
} }
} }
@ -137,8 +143,9 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
if (!_localFileSystem.FileExists(path)) if (!_localFileSystem.FileExists(path))
{ {
_logger.LogInformation("Flagging missing episode at {Path}", path); _logger.LogInformation("Flagging missing episode at {Path}", path);
List<int> episodeIds = await FlagFileNotFound(libraryPath, path); List<int> episodeIds = await FlagFileNotFound(libraryPath, path);
await _searchIndex.RebuildItems(_searchRepository, episodeIds); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, episodeIds);
} }
else if (Path.GetFileName(path).StartsWith("._")) else if (Path.GetFileName(path).StartsWith("._"))
{ {
@ -241,7 +248,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
await _libraryRepository.SetEtag(libraryPath, knownFolder, seasonFolder, etag); await _libraryRepository.SetEtag(libraryPath, knownFolder, seasonFolder, etag);
season.Show = show; season.Show = show;
await _searchIndex.RebuildItems(_searchRepository, new List<int> { season.Id }); await _searchIndex.RebuildItems(
_searchRepository,
_fallbackMetadataProvider,
new List<int> { season.Id });
} }
} }
} }
@ -287,7 +297,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
foreach (Episode episode in maybeEpisode.RightToSeq()) foreach (Episode episode in maybeEpisode.RightToSeq())
{ {
await _searchIndex.RebuildItems(_searchRepository, new List<int> { episode.Id }); await _searchIndex.RebuildItems(
_searchRepository,
_fallbackMetadataProvider,
new List<int> { episode.Id });
} }
} }

2
ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs

@ -28,6 +28,7 @@ public class PlexMovieLibraryScanner :
IMetadataRepository metadataRepository, IMetadataRepository metadataRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
ISearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IMediator mediator, IMediator mediator,
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
IPlexMovieRepository plexMovieRepository, IPlexMovieRepository plexMovieRepository,
@ -43,6 +44,7 @@ public class PlexMovieLibraryScanner :
mediator, mediator,
searchIndex, searchIndex,
searchRepository, searchRepository,
fallbackMetadataProvider,
logger) logger)
{ {
_plexServerApiClient = plexServerApiClient; _plexServerApiClient = plexServerApiClient;

2
ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs

@ -28,6 +28,7 @@ public class PlexTelevisionLibraryScanner :
IMetadataRepository metadataRepository, IMetadataRepository metadataRepository,
ISearchIndex searchIndex, ISearchIndex searchIndex,
ISearchRepository searchRepository, ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IMediator mediator, IMediator mediator,
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
IPlexPathReplacementService plexPathReplacementService, IPlexPathReplacementService plexPathReplacementService,
@ -42,6 +43,7 @@ public class PlexTelevisionLibraryScanner :
localFileSystem, localFileSystem,
searchRepository, searchRepository,
searchIndex, searchIndex,
fallbackMetadataProvider,
mediator, mediator,
logger) logger)
{ {

3
ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs

@ -1,4 +1,3 @@
using System.Numerics;
using ErsatzTV.FFmpeg.Format; using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.FFmpeg.Capabilities; namespace ErsatzTV.FFmpeg.Capabilities;
@ -42,7 +41,7 @@ public class NvidiaHardwareCapabilities : IHardwareCapabilities
// nvidia cannot encode 10-bit h264 // nvidia cannot encode 10-bit h264
VideoFormat.H264 when bitDepth == 10 => false, VideoFormat.H264 when bitDepth == 10 => false,
_ => true _ => true
}; };
} }

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

@ -9,6 +9,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories;
public class SearchRepository : ISearchRepository public class SearchRepository : ISearchRepository
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly SemaphoreSlim _slim = new(1, 1);
private List<string> _allLanguageCodes;
public SearchRepository(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory; public SearchRepository(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
@ -49,6 +51,8 @@ public class SearchRepository : ISearchRepository
.ThenInclude(em => em.Guids) .ThenInclude(em => em.Guids)
.Include(mi => (mi as Episode).MediaVersions) .Include(mi => (mi as Episode).MediaVersions)
.ThenInclude(em => em.Streams) .ThenInclude(em => em.Streams)
.Include(mi => (mi as Episode).MediaVersions)
.ThenInclude(em => em.MediaFiles)
.Include(mi => (mi as Episode).Season) .Include(mi => (mi as Episode).Season)
.ThenInclude(s => s.Show) .ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata) .ThenInclude(s => s.ShowMetadata)
@ -162,8 +166,24 @@ public class SearchRepository : ISearchRepository
public async Task<List<string>> GetAllLanguageCodes(List<string> mediaCodes) public async Task<List<string>> GetAllLanguageCodes(List<string> mediaCodes)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); if (_allLanguageCodes == null)
return await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCodes); {
await _slim.WaitAsync();
try
{
if (_allLanguageCodes == null)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
_allLanguageCodes = await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCodes);
}
}
finally
{
_slim.Release();
}
}
return _allLanguageCodes;
} }
public IAsyncEnumerable<MediaItem> GetAllMediaItems() public IAsyncEnumerable<MediaItem> GetAllMediaItems()
@ -203,6 +223,8 @@ public class SearchRepository : ISearchRepository
.ThenInclude(em => em.Guids) .ThenInclude(em => em.Guids)
.Include(mi => (mi as Episode).MediaVersions) .Include(mi => (mi as Episode).MediaVersions)
.ThenInclude(em => em.Streams) .ThenInclude(em => em.Streams)
.Include(mi => (mi as Episode).MediaVersions)
.ThenInclude(em => em.MediaFiles)
.Include(mi => (mi as Episode).Season) .Include(mi => (mi as Episode).Season)
.ThenInclude(s => s.Show) .ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata) .ThenInclude(s => s.ShowMetadata)

45
ErsatzTV.Infrastructure/Search/SearchIndex.cs

@ -88,7 +88,7 @@ public sealed class SearchIndex : ISearchIndex
_initialized = false; _initialized = false;
} }
public int Version => 27; public int Version => 28;
public async Task<bool> Initialize( public async Task<bool> Initialize(
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
@ -117,7 +117,10 @@ public sealed class SearchIndex : ISearchIndex
return _initialized; return _initialized;
} }
public async Task<Unit> UpdateItems(ISearchRepository searchRepository, List<MediaItem> items) public async Task<Unit> UpdateItems(
ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
List<MediaItem> items)
{ {
foreach (MediaItem item in items) foreach (MediaItem item in items)
{ {
@ -139,7 +142,7 @@ public sealed class SearchIndex : ISearchIndex
await UpdateMusicVideo(searchRepository, musicVideo); await UpdateMusicVideo(searchRepository, musicVideo);
break; break;
case Episode episode: case Episode episode:
await UpdateEpisode(searchRepository, episode); await UpdateEpisode(searchRepository, fallbackMetadataProvider, episode);
break; break;
case OtherVideo otherVideo: case OtherVideo otherVideo:
await UpdateOtherVideo(searchRepository, otherVideo); await UpdateOtherVideo(searchRepository, otherVideo);
@ -215,27 +218,32 @@ public sealed class SearchIndex : ISearchIndex
_directory?.Dispose(); _directory?.Dispose();
} }
public async Task<Unit> Rebuild(ISearchRepository searchRepository) public async Task<Unit> Rebuild(
ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider)
{ {
_writer.DeleteAll(); _writer.DeleteAll();
_writer.Commit(); _writer.Commit();
await foreach (MediaItem mediaItem in searchRepository.GetAllMediaItems()) await foreach (MediaItem mediaItem in searchRepository.GetAllMediaItems())
{ {
await RebuildItem(searchRepository, mediaItem); await RebuildItem(searchRepository, fallbackMetadataProvider, mediaItem);
} }
_writer.Commit(); _writer.Commit();
return Unit.Default; return Unit.Default;
} }
public async Task<Unit> RebuildItems(ISearchRepository searchRepository, List<int> itemIds) public async Task<Unit> RebuildItems(
ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
List<int> itemIds)
{ {
foreach (int id in itemIds) foreach (int id in itemIds)
{ {
foreach (MediaItem mediaItem in await searchRepository.GetItemToIndex(id)) foreach (MediaItem mediaItem in await searchRepository.GetItemToIndex(id))
{ {
await RebuildItem(searchRepository, mediaItem); await RebuildItem(searchRepository, fallbackMetadataProvider, mediaItem);
} }
} }
@ -268,7 +276,10 @@ public sealed class SearchIndex : ISearchIndex
} }
} }
private async Task RebuildItem(ISearchRepository searchRepository, MediaItem mediaItem) private async Task RebuildItem(
ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
MediaItem mediaItem)
{ {
switch (mediaItem) switch (mediaItem)
{ {
@ -288,7 +299,7 @@ public sealed class SearchIndex : ISearchIndex
await UpdateMusicVideo(searchRepository, musicVideo); await UpdateMusicVideo(searchRepository, musicVideo);
break; break;
case Episode episode: case Episode episode:
await UpdateEpisode(searchRepository, episode); await UpdateEpisode(searchRepository, fallbackMetadataProvider, episode);
break; break;
case OtherVideo otherVideo: case OtherVideo otherVideo:
await UpdateOtherVideo(searchRepository, otherVideo); await UpdateOtherVideo(searchRepository, otherVideo);
@ -807,8 +818,22 @@ public sealed class SearchIndex : ISearchIndex
} }
} }
private async Task UpdateEpisode(ISearchRepository searchRepository, Episode episode) private async Task UpdateEpisode(
ISearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
Episode episode)
{ {
// try to load metadata here, since episodes without metadata won't index
if (episode.EpisodeMetadata.Count == 0)
{
episode.EpisodeMetadata ??= new List<EpisodeMetadata>();
episode.EpisodeMetadata = fallbackMetadataProvider.GetFallbackMetadata(episode);
foreach (EpisodeMetadata metadata in episode.EpisodeMetadata)
{
metadata.Episode = episode;
}
}
foreach (EpisodeMetadata metadata in episode.EpisodeMetadata) foreach (EpisodeMetadata metadata in episode.EpisodeMetadata)
{ {
try try

Loading…
Cancel
Save