search overhaul (#87)

* add letter bar with no links

* use lucene for search, add paged search results

* add search index version

* index library_name; rebuild index when folder is missing

* maintain index as local movies change

* fix tests

* maintain index as local shows change

* maintain index as plex movies change

* maintain index as plex shows change

* code cleanup

* add duplicate filter to search

* add links to letter bar

* code cleanup
This commit is contained in:
Jason Dove
2021-03-20 15:49:50 +00:00
committed by GitHub
parent f96efa9b2f
commit 2efcbca2da
69 changed files with 1351 additions and 324 deletions
+51 -18
View File
@@ -4,9 +4,10 @@ using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata;
using LanguageExt;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Plex
{
@@ -16,17 +17,20 @@ namespace ErsatzTV.Core.Plex
private readonly IMetadataRepository _metadataRepository;
private readonly IMovieRepository _movieRepository;
private readonly IPlexServerApiClient _plexServerApiClient;
private readonly ISearchIndex _searchIndex;
public PlexMovieLibraryScanner(
IPlexServerApiClient plexServerApiClient,
IMovieRepository movieRepository,
IMetadataRepository metadataRepository,
ISearchIndex searchIndex,
ILogger<PlexMovieLibraryScanner> logger)
: base(metadataRepository)
{
_plexServerApiClient = plexServerApiClient;
_movieRepository = movieRepository;
_metadataRepository = metadataRepository;
_searchIndex = searchIndex;
_logger = logger;
}
@@ -46,21 +50,37 @@ namespace ErsatzTV.Core.Plex
foreach (PlexMovie incoming in movieEntries)
{
// TODO: figure out how to rebuild playlists
Either<BaseError, PlexMovie> maybeMovie = await _movieRepository
Either<BaseError, MediaItemScanResult<PlexMovie>> maybeMovie = await _movieRepository
.GetOrAdd(plexMediaSourceLibrary, incoming)
.BindT(existing => UpdateStatistics(existing, incoming, connection, token))
.BindT(existing => UpdateMetadata(existing, incoming))
.BindT(existing => UpdateArtwork(existing, incoming));
maybeMovie.IfLeft(
error => _logger.LogWarning(
"Error processing plex movie at {Key}: {Error}",
incoming.Key,
error.Value));
await maybeMovie.Match(
async result =>
{
if (result.IsAdded)
{
await _searchIndex.AddItems(new List<MediaItem> { result.Item });
}
else if (result.IsUpdated)
{
await _searchIndex.UpdateItems(new List<MediaItem> { result.Item });
}
},
error =>
{
_logger.LogWarning(
"Error processing plex movie at {Key}: {Error}",
incoming.Key,
error.Value);
return Task.CompletedTask;
});
}
var movieKeys = movieEntries.Map(s => s.Key).ToList();
await _movieRepository.RemoveMissingPlexMovies(plexMediaSourceLibrary, movieKeys);
List<int> ids = await _movieRepository.RemoveMissingPlexMovies(plexMediaSourceLibrary, movieKeys);
await _searchIndex.RemoveItems(ids);
},
error =>
{
@@ -75,12 +95,13 @@ namespace ErsatzTV.Core.Plex
return Unit.Default;
}
private async Task<Either<BaseError, PlexMovie>> UpdateStatistics(
PlexMovie existing,
private async Task<Either<BaseError, MediaItemScanResult<PlexMovie>>> UpdateStatistics(
MediaItemScanResult<PlexMovie> result,
PlexMovie incoming,
PlexConnection connection,
PlexServerAuthToken token)
{
PlexMovie existing = result.Item;
MediaVersion existingVersion = existing.MediaVersions.Head();
MediaVersion incomingVersion = incoming.MediaVersions.Head();
@@ -102,11 +123,14 @@ namespace ErsatzTV.Core.Plex
_ => Task.CompletedTask);
}
return Right<BaseError, PlexMovie>(existing);
return result;
}
private async Task<Either<BaseError, PlexMovie>> UpdateMetadata(PlexMovie existing, PlexMovie incoming)
private async Task<Either<BaseError, MediaItemScanResult<PlexMovie>>> UpdateMetadata(
MediaItemScanResult<PlexMovie> result,
PlexMovie incoming)
{
PlexMovie existing = result.Item;
MovieMetadata existingMetadata = existing.MovieMetadata.Head();
MovieMetadata incomingMetadata = incoming.MovieMetadata.Head();
@@ -117,7 +141,10 @@ namespace ErsatzTV.Core.Plex
.ToList())
{
existingMetadata.Genres.Remove(genre);
await _metadataRepository.RemoveGenre(genre);
if (await _metadataRepository.RemoveGenre(genre))
{
result.IsUpdated = true;
}
}
foreach (Genre genre in incomingMetadata.Genres
@@ -125,17 +152,23 @@ namespace ErsatzTV.Core.Plex
.ToList())
{
existingMetadata.Genres.Add(genre);
await _movieRepository.AddGenre(existingMetadata, genre);
if (await _movieRepository.AddGenre(existingMetadata, genre))
{
result.IsUpdated = true;
}
}
// TODO: update other metadata?
}
return existing;
return result;
}
private async Task<Either<BaseError, PlexMovie>> UpdateArtwork(PlexMovie existing, PlexMovie incoming)
private async Task<Either<BaseError, MediaItemScanResult<PlexMovie>>> UpdateArtwork(
MediaItemScanResult<PlexMovie> result,
PlexMovie incoming)
{
PlexMovie existing = result.Item;
MovieMetadata existingMetadata = existing.MovieMetadata.Head();
MovieMetadata incomingMetadata = incoming.MovieMetadata.Head();
@@ -145,7 +178,7 @@ namespace ErsatzTV.Core.Plex
await UpdateArtworkIfNeeded(existingMetadata, incomingMetadata, ArtworkKind.FanArt);
}
return existing;
return result;
}
}
}
@@ -4,6 +4,8 @@ using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata;
using LanguageExt;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
@@ -15,18 +17,21 @@ namespace ErsatzTV.Core.Plex
private readonly ILogger<PlexTelevisionLibraryScanner> _logger;
private readonly IMetadataRepository _metadataRepository;
private readonly IPlexServerApiClient _plexServerApiClient;
private readonly ISearchIndex _searchIndex;
private readonly ITelevisionRepository _televisionRepository;
public PlexTelevisionLibraryScanner(
IPlexServerApiClient plexServerApiClient,
ITelevisionRepository televisionRepository,
IMetadataRepository metadataRepository,
ISearchIndex searchIndex,
ILogger<PlexTelevisionLibraryScanner> logger)
: base(metadataRepository)
{
_plexServerApiClient = plexServerApiClient;
_televisionRepository = televisionRepository;
_metadataRepository = metadataRepository;
_searchIndex = searchIndex;
_logger = logger;
}
@@ -46,13 +51,25 @@ namespace ErsatzTV.Core.Plex
foreach (PlexShow incoming in showEntries)
{
// TODO: figure out how to rebuild playlists
Either<BaseError, PlexShow> maybeShow = await _televisionRepository
Either<BaseError, MediaItemScanResult<PlexShow>> maybeShow = await _televisionRepository
.GetOrAddPlexShow(plexMediaSourceLibrary, incoming)
.BindT(existing => UpdateMetadata(existing, incoming))
.BindT(existing => UpdateArtwork(existing, incoming));
await maybeShow.Match(
async show => await ScanSeasons(plexMediaSourceLibrary, show, connection, token),
async result =>
{
if (result.IsAdded)
{
await _searchIndex.AddItems(new List<MediaItem> { result.Item });
}
else if (result.IsUpdated)
{
await _searchIndex.UpdateItems(new List<MediaItem> { result.Item });
}
await ScanSeasons(plexMediaSourceLibrary, result.Item, connection, token);
},
error =>
{
_logger.LogWarning(
@@ -64,7 +81,9 @@ namespace ErsatzTV.Core.Plex
}
var showKeys = showEntries.Map(s => s.Key).ToList();
await _televisionRepository.RemoveMissingPlexShows(plexMediaSourceLibrary, showKeys);
List<int> ids =
await _televisionRepository.RemoveMissingPlexShows(plexMediaSourceLibrary, showKeys);
await _searchIndex.RemoveItems(ids);
return Unit.Default;
},
@@ -79,8 +98,11 @@ namespace ErsatzTV.Core.Plex
});
}
private Task<Either<BaseError, PlexShow>> UpdateMetadata(PlexShow existing, PlexShow incoming)
private async Task<Either<BaseError, MediaItemScanResult<PlexShow>>> UpdateMetadata(
MediaItemScanResult<PlexShow> result,
PlexShow incoming)
{
PlexShow existing = result.Item;
ShowMetadata existingMetadata = existing.ShowMetadata.Head();
ShowMetadata incomingMetadata = incoming.ShowMetadata.Head();
@@ -93,7 +115,10 @@ namespace ErsatzTV.Core.Plex
.ToList())
{
existingMetadata.Genres.Remove(genre);
_metadataRepository.RemoveGenre(genre);
if (await _metadataRepository.RemoveGenre(genre))
{
result.IsUpdated = true;
}
}
foreach (Genre genre in incomingMetadata.Genres
@@ -101,15 +126,21 @@ namespace ErsatzTV.Core.Plex
.ToList())
{
existingMetadata.Genres.Add(genre);
_televisionRepository.AddGenre(existingMetadata, genre);
if (await _televisionRepository.AddGenre(existingMetadata, genre))
{
result.IsUpdated = true;
}
}
}
return Right<BaseError, PlexShow>(existing).AsTask();
return result;
}
private async Task<Either<BaseError, PlexShow>> UpdateArtwork(PlexShow existing, PlexShow incoming)
private async Task<Either<BaseError, MediaItemScanResult<PlexShow>>> UpdateArtwork(
MediaItemScanResult<PlexShow> result,
PlexShow incoming)
{
PlexShow existing = result.Item;
ShowMetadata existingMetadata = existing.ShowMetadata.Head();
ShowMetadata incomingMetadata = incoming.ShowMetadata.Head();
@@ -119,7 +150,7 @@ namespace ErsatzTV.Core.Plex
await UpdateArtworkIfNeeded(existingMetadata, incomingMetadata, ArtworkKind.FanArt);
}
return existing;
return result;
}
private async Task<Either<BaseError, Unit>> ScanSeasons(