mirror of https://github.com/ErsatzTV/ErsatzTV.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1492 lines
61 KiB
1492 lines
61 KiB
using System.Globalization; |
|
using Bugsnag; |
|
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; |
|
using ErsatzTV.FFmpeg.Format; |
|
using LanguageExt.UnsafeValueAccess; |
|
using Lucene.Net.Analysis; |
|
using Lucene.Net.Documents; |
|
using Lucene.Net.Index; |
|
using Lucene.Net.Sandbox.Queries; |
|
using Lucene.Net.Search; |
|
using Lucene.Net.Store; |
|
using Lucene.Net.Util; |
|
using Microsoft.Extensions.Logging; |
|
using Directory = System.IO.Directory; |
|
using MediaStream = ErsatzTV.Core.Domain.MediaStream; |
|
using Query = Lucene.Net.Search.Query; |
|
|
|
namespace ErsatzTV.Infrastructure.Search; |
|
|
|
public sealed class LuceneSearchIndex : ISearchIndex |
|
{ |
|
internal const LuceneVersion AppLuceneVersion = LuceneVersion.LUCENE_48; |
|
|
|
internal const string IdField = "id"; |
|
internal const string TypeField = "type"; |
|
internal const string TitleField = "title"; |
|
internal const string SortTitleField = "sort_title"; |
|
internal const string GenreField = "genre"; |
|
internal const string TagField = "tag"; |
|
internal const string TagFullField = "tag_full"; |
|
internal const string PlotField = "plot"; |
|
internal const string LibraryNameField = "library_name"; |
|
internal const string LibraryIdField = "library_id"; |
|
internal const string LibraryFolderIdField = "library_folder_id"; |
|
internal const string TitleAndYearField = "title_and_year"; |
|
internal const string JumpLetterField = "jump_letter"; |
|
internal const string StudioField = "studio"; |
|
internal const string LanguageField = "language"; |
|
internal const string LanguageTagField = "language_tag"; |
|
internal const string SubLanguageField = "sub_language"; |
|
internal const string SubLanguageTagField = "sub_language_tag"; |
|
internal const string StyleField = "style"; |
|
internal const string MoodField = "mood"; |
|
internal const string ActorField = "actor"; |
|
internal const string ContentRatingField = "content_rating"; |
|
internal const string DirectorField = "director"; |
|
internal const string WriterField = "writer"; |
|
internal const string TraktListField = "trakt_list"; |
|
internal const string AlbumField = "album"; |
|
internal const string ArtistField = "artist"; |
|
internal const string StateField = "state"; |
|
internal const string AlbumArtistField = "album_artist"; |
|
internal const string ShowTitleField = "show_title"; |
|
internal const string ShowGenreField = "show_genre"; |
|
internal const string ShowTagField = "show_tag"; |
|
internal const string ShowStudioField = "show_studio"; |
|
internal const string ShowContentRatingField = "show_content_rating"; |
|
internal const string MetadataKindField = "metadata_kind"; |
|
internal const string VideoCodecField = "video_codec"; |
|
internal const string VideoDynamicRangeField = "video_dynamic_range"; |
|
internal const string CollectionField = "collection"; |
|
|
|
internal const string MinutesField = "minutes"; |
|
internal const string SecondsField = "seconds"; |
|
internal const string HeightField = "height"; |
|
internal const string WidthField = "width"; |
|
internal const string SeasonNumberField = "season_number"; |
|
internal const string EpisodeNumberField = "episode_number"; |
|
internal const string AddedDateField = "added_date"; |
|
internal const string ReleaseDateField = "release_date"; |
|
internal const string VideoBitDepthField = "video_bit_depth"; |
|
|
|
public const string MovieType = "movie"; |
|
public const string ShowType = "show"; |
|
public const string SeasonType = "season"; |
|
public const string ArtistType = "artist"; |
|
public const string MusicVideoType = "music_video"; |
|
public const string EpisodeType = "episode"; |
|
public const string OtherVideoType = "other_video"; |
|
public const string SongType = "song"; |
|
public const string ImageType = "image"; |
|
private readonly string _cleanShutdownPath; |
|
|
|
private readonly List<CultureInfo> _cultureInfos; |
|
|
|
private readonly SearchQueryParser _searchQueryParser; |
|
private readonly ILogger<LuceneSearchIndex> _logger; |
|
|
|
private FSDirectory _directory; |
|
private bool _initialized; |
|
private IndexWriter _writer; |
|
|
|
public LuceneSearchIndex(SearchQueryParser searchQueryParser, ILogger<LuceneSearchIndex> logger) |
|
{ |
|
_searchQueryParser = searchQueryParser; |
|
_logger = logger; |
|
_cultureInfos = CultureInfo.GetCultures(CultureTypes.NeutralCultures).ToList(); |
|
_cleanShutdownPath = Path.Combine(FileSystemLayout.SearchIndexFolder, ".clean-shutdown"); |
|
_initialized = false; |
|
} |
|
|
|
public Task<bool> IndexExists() |
|
{ |
|
bool directoryExists = Directory.Exists(FileSystemLayout.SearchIndexFolder); |
|
bool fileExists = File.Exists(_cleanShutdownPath); |
|
|
|
return Task.FromResult(directoryExists && fileExists); |
|
} |
|
|
|
public int Version => 46; |
|
|
|
public async Task<bool> Initialize( |
|
ILocalFileSystem localFileSystem, |
|
IConfigElementRepository configElementRepository) |
|
{ |
|
if (!_initialized) |
|
{ |
|
localFileSystem.EnsureFolderExists(FileSystemLayout.SearchIndexFolder); |
|
|
|
if (!ValidateDirectory(FileSystemLayout.SearchIndexFolder)) |
|
{ |
|
_logger.LogWarning("Search index failed to initialize; will delete and recreate"); |
|
await configElementRepository.Upsert(ConfigElementKey.SearchIndexVersion, 0); |
|
Directory.Delete(FileSystemLayout.SearchIndexFolder, true); |
|
localFileSystem.EnsureFolderExists(FileSystemLayout.SearchIndexFolder); |
|
} |
|
|
|
if (File.Exists(_cleanShutdownPath)) |
|
{ |
|
File.Delete(_cleanShutdownPath); |
|
} |
|
|
|
_directory = FSDirectory.Open(FileSystemLayout.SearchIndexFolder); |
|
Analyzer analyzer = SearchQueryParser.AnalyzerWrapper(); |
|
var indexConfig = new IndexWriterConfig(AppLuceneVersion, analyzer) |
|
{ OpenMode = OpenMode.CREATE_OR_APPEND }; |
|
_writer = new IndexWriter(_directory, indexConfig); |
|
_initialized = true; |
|
} |
|
|
|
return _initialized; |
|
} |
|
|
|
public async Task<Unit> UpdateItems( |
|
ICachingSearchRepository searchRepository, |
|
IFallbackMetadataProvider fallbackMetadataProvider, |
|
List<MediaItem> items) |
|
{ |
|
foreach (MediaItem item in items) |
|
{ |
|
switch (item) |
|
{ |
|
case Movie movie: |
|
await UpdateMovie(searchRepository, movie); |
|
break; |
|
case Show show: |
|
await UpdateShow(searchRepository, show); |
|
break; |
|
case Season season: |
|
await UpdateSeason(searchRepository, season); |
|
break; |
|
case Artist artist: |
|
await UpdateArtist(searchRepository, artist); |
|
break; |
|
case MusicVideo musicVideo: |
|
await UpdateMusicVideo(searchRepository, musicVideo); |
|
break; |
|
case Episode episode: |
|
await UpdateEpisode(searchRepository, fallbackMetadataProvider, episode); |
|
break; |
|
case OtherVideo otherVideo: |
|
await UpdateOtherVideo(searchRepository, otherVideo); |
|
break; |
|
case Song song: |
|
await UpdateSong(searchRepository, song); |
|
break; |
|
case Image image: |
|
await UpdateImage(searchRepository, image); |
|
break; |
|
} |
|
} |
|
|
|
return Unit.Default; |
|
} |
|
|
|
public Task<bool> RemoveItems(IEnumerable<int> ids) |
|
{ |
|
foreach (int id in ids) |
|
{ |
|
_writer.DeleteDocuments(new Term(IdField, id.ToString(CultureInfo.InvariantCulture))); |
|
} |
|
|
|
return Task.FromResult(true); |
|
} |
|
|
|
public async Task<SearchResult> Search(IClient client, string query, string smartCollectionName, int skip, int limit) |
|
{ |
|
var metadata = new Dictionary<string, string> |
|
{ |
|
{ "searchQuery", query }, |
|
{ "skip", skip.ToString(CultureInfo.InvariantCulture) }, |
|
{ "limit", limit.ToString(CultureInfo.InvariantCulture) } |
|
}; |
|
|
|
client?.Breadcrumbs?.Leave("SearchIndex.Search", BreadcrumbType.State, metadata); |
|
|
|
if (string.IsNullOrWhiteSpace(query.Replace("*", string.Empty).Replace("?", string.Empty)) || |
|
_writer.MaxDoc == 0) |
|
{ |
|
return new SearchResult([], 0); |
|
} |
|
|
|
using DirectoryReader reader = _writer.GetReader(true); |
|
var searcher = new IndexSearcher(reader); |
|
int hitsLimit = limit == 0 ? searcher.IndexReader.MaxDoc : skip + limit; |
|
Query parsedQuery = await _searchQueryParser.ParseQuery(query, smartCollectionName); |
|
// TODO: figure out if this is actually needed |
|
// var filter = new DuplicateFilter(TitleAndYearField); |
|
var sort = new Sort(new SortField(SortTitleField, SortFieldType.STRING)); |
|
TopFieldDocs topDocs = searcher.Search(parsedQuery, null, hitsLimit, sort, true, true); |
|
IEnumerable<ScoreDoc> selectedHits = topDocs.ScoreDocs.Skip(skip); |
|
|
|
if (limit is > 0 and < 10_000) |
|
{ |
|
selectedHits = selectedHits.Take(limit); |
|
} |
|
|
|
var searchResult = new SearchResult( |
|
selectedHits.Map(d => ProjectToSearchItem(searcher.Doc(d.Doc))).ToList(), |
|
topDocs.TotalHits); |
|
|
|
if (limit is > 0 and < 10_000) |
|
{ |
|
searchResult.PageMap = GetSearchPageMap(searcher, parsedQuery, null, sort, limit); |
|
} |
|
|
|
return searchResult; |
|
} |
|
|
|
public void Commit() => _writer.Commit(); |
|
|
|
public void Dispose() |
|
{ |
|
_writer?.Dispose(); |
|
_directory?.Dispose(); |
|
|
|
using (File.Create(_cleanShutdownPath)) |
|
{ |
|
// do nothing |
|
} |
|
} |
|
|
|
public async Task<Unit> Rebuild( |
|
ICachingSearchRepository searchRepository, |
|
IFallbackMetadataProvider fallbackMetadataProvider) |
|
{ |
|
_writer.DeleteAll(); |
|
_writer.Commit(); |
|
|
|
await foreach (MediaItem mediaItem in searchRepository.GetAllMediaItems()) |
|
{ |
|
await RebuildItem(searchRepository, fallbackMetadataProvider, mediaItem); |
|
} |
|
|
|
_writer.Commit(); |
|
return Unit.Default; |
|
} |
|
|
|
public async Task<Unit> RebuildItems( |
|
ICachingSearchRepository searchRepository, |
|
IFallbackMetadataProvider fallbackMetadataProvider, |
|
IEnumerable<int> itemIds) |
|
{ |
|
foreach (int id in itemIds) |
|
{ |
|
foreach (MediaItem mediaItem in await searchRepository.GetItemToIndex(id)) |
|
{ |
|
await RebuildItem(searchRepository, fallbackMetadataProvider, mediaItem); |
|
} |
|
} |
|
|
|
return Unit.Default; |
|
} |
|
|
|
private bool ValidateDirectory(string folder) |
|
{ |
|
try |
|
{ |
|
using (var d = FSDirectory.Open(folder)) |
|
{ |
|
using (Analyzer analyzer = SearchQueryParser.AnalyzerWrapper()) |
|
{ |
|
var indexConfig = new IndexWriterConfig(AppLuceneVersion, analyzer) |
|
{ OpenMode = OpenMode.CREATE_OR_APPEND }; |
|
using (var w = new IndexWriter(d, indexConfig)) |
|
{ |
|
using (DirectoryReader _ = w.GetReader(true)) |
|
{ |
|
return File.Exists(_cleanShutdownPath); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
catch |
|
{ |
|
return false; |
|
} |
|
} |
|
|
|
private async Task RebuildItem( |
|
ISearchRepository searchRepository, |
|
IFallbackMetadataProvider fallbackMetadataProvider, |
|
MediaItem mediaItem) |
|
{ |
|
switch (mediaItem) |
|
{ |
|
case Movie movie: |
|
await UpdateMovie(searchRepository, movie); |
|
break; |
|
case Show show: |
|
await UpdateShow(searchRepository, show); |
|
break; |
|
case Season season: |
|
await UpdateSeason(searchRepository, season); |
|
break; |
|
case Artist artist: |
|
await UpdateArtist(searchRepository, artist); |
|
break; |
|
case MusicVideo musicVideo: |
|
await UpdateMusicVideo(searchRepository, musicVideo); |
|
break; |
|
case Episode episode: |
|
await UpdateEpisode(searchRepository, fallbackMetadataProvider, episode); |
|
break; |
|
case OtherVideo otherVideo: |
|
await UpdateOtherVideo(searchRepository, otherVideo); |
|
break; |
|
case Song song: |
|
await UpdateSong(searchRepository, song); |
|
break; |
|
case Image image: |
|
await UpdateImage(searchRepository, image); |
|
break; |
|
} |
|
} |
|
|
|
private static SearchPageMap GetSearchPageMap( |
|
IndexSearcher searcher, |
|
Query query, |
|
DuplicateFilter filter, |
|
Sort sort, |
|
int limit) |
|
{ |
|
ScoreDoc[] allDocs = searcher.Search(query, filter, int.MaxValue, sort, true, true).ScoreDocs; |
|
var letters = new List<char> |
|
{ |
|
'#', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', |
|
'u', 'v', 'w', 'x', 'y', 'z' |
|
}; |
|
var map = letters.ToDictionary(letter => letter, _ => 0); |
|
|
|
var current = 0; |
|
var page = 0; |
|
while (current < allDocs.Length) |
|
{ |
|
// walk up by page size (limit) |
|
page++; |
|
current += limit; |
|
if (current > allDocs.Length) |
|
{ |
|
current = allDocs.Length; |
|
} |
|
|
|
char jumpLetter = searcher.Doc(allDocs[current - 1].Doc).Get(JumpLetterField, CultureInfo.InvariantCulture) |
|
.Head(); |
|
foreach (char letter in letters.Where(l => letters.IndexOf(l) <= letters.IndexOf(jumpLetter))) |
|
{ |
|
if (map[letter] == 0) |
|
{ |
|
map[letter] = page; |
|
} |
|
} |
|
} |
|
|
|
int max = map.Values.Max(); |
|
foreach (char letter in letters.Where(letter => map[letter] == 0)) |
|
{ |
|
map[letter] = max; |
|
} |
|
|
|
return new SearchPageMap(map); |
|
} |
|
|
|
private async Task UpdateMovie(ISearchRepository searchRepository, Movie movie) |
|
{ |
|
Option<MovieMetadata> maybeMetadata = movie.MovieMetadata.HeadOrNone(); |
|
if (maybeMetadata.IsSome) |
|
{ |
|
MovieMetadata metadata = maybeMetadata.ValueUnsafe(); |
|
|
|
try |
|
{ |
|
var doc = new Document |
|
{ |
|
new StringField(IdField, movie.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES), |
|
new StringField(TypeField, MovieType, Field.Store.YES), |
|
new TextField(TitleField, metadata.Title, Field.Store.NO), |
|
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO), |
|
new TextField(LibraryNameField, movie.LibraryPath.Library.Name, Field.Store.NO), |
|
new StringField( |
|
LibraryIdField, |
|
movie.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture), |
|
Field.Store.NO), |
|
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO), |
|
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES), |
|
new StringField(StateField, movie.State.ToString(), Field.Store.NO), |
|
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) |
|
}; |
|
|
|
await AddLanguages(searchRepository, doc, movie.MediaVersions); |
|
|
|
AddStatistics(doc, movie.MediaVersions); |
|
AddCollections(doc, movie.Collections); |
|
|
|
if (!string.IsNullOrWhiteSpace(metadata.ContentRating)) |
|
{ |
|
foreach (string contentRating in (metadata.ContentRating ?? string.Empty).Split("/") |
|
.Map(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x))) |
|
{ |
|
doc.Add(new StringField(ContentRatingField, contentRating, Field.Store.NO)); |
|
} |
|
} |
|
|
|
if (metadata.ReleaseDate.HasValue) |
|
{ |
|
doc.Add( |
|
new StringField( |
|
ReleaseDateField, |
|
metadata.ReleaseDate.Value.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
} |
|
|
|
doc.Add( |
|
new StringField( |
|
AddedDateField, |
|
metadata.DateAdded.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
|
|
if (!string.IsNullOrWhiteSpace(metadata.Plot)) |
|
{ |
|
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO)); |
|
} |
|
|
|
foreach (Genre genre in metadata.Genres) |
|
{ |
|
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Tag tag in metadata.Tags) |
|
{ |
|
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO)); |
|
doc.Add(new StringField(TagFullField, tag.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Studio studio in metadata.Studios) |
|
{ |
|
doc.Add(new TextField(StudioField, studio.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Actor actor in metadata.Actors) |
|
{ |
|
doc.Add(new TextField(ActorField, actor.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Director director in metadata.Directors) |
|
{ |
|
doc.Add(new TextField(DirectorField, director.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Writer writer in metadata.Writers) |
|
{ |
|
doc.Add(new TextField(WriterField, writer.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (TraktListItem item in movie.TraktListItems) |
|
{ |
|
doc.Add( |
|
new StringField( |
|
TraktListField, |
|
item.TraktList.TraktId.ToString(CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
} |
|
|
|
AddMetadataGuids(metadata, doc); |
|
|
|
_writer.UpdateDocument(new Term(IdField, movie.Id.ToString(CultureInfo.InvariantCulture)), doc); |
|
} |
|
catch (Exception ex) |
|
{ |
|
metadata.Movie = null; |
|
_logger.LogWarning(ex, "Error indexing movie with metadata {@Metadata}", metadata); |
|
} |
|
} |
|
} |
|
|
|
private async Task AddLanguages( |
|
ISearchRepository searchRepository, |
|
Document doc, |
|
ICollection<MediaVersion> mediaVersions) |
|
{ |
|
var mediaCodes = mediaVersions |
|
.Map(mv => mv.Streams.Filter(ms => ms.MediaStreamKind == MediaStreamKind.Audio).Map(ms => ms.Language)) |
|
.Flatten() |
|
.Filter(c => !string.IsNullOrWhiteSpace(c)) |
|
.Distinct() |
|
.ToList(); |
|
|
|
await AddLanguages(searchRepository, doc, mediaCodes); |
|
|
|
var subMediaCodes = mediaVersions |
|
.Map( |
|
mv => mv.Streams |
|
.Filter(ms => ms.MediaStreamKind is MediaStreamKind.Subtitle or MediaStreamKind.ExternalSubtitle) |
|
.Map(ms => ms.Language)) |
|
.Flatten() |
|
.Filter(c => !string.IsNullOrWhiteSpace(c)) |
|
.Distinct() |
|
.ToList(); |
|
|
|
await AddSubLanguages(searchRepository, doc, subMediaCodes); |
|
} |
|
|
|
private async Task AddLanguages(ISearchRepository searchRepository, Document doc, List<string> mediaCodes) |
|
{ |
|
foreach (string code in mediaCodes.Where(c => !string.IsNullOrWhiteSpace(c)).Distinct()) |
|
{ |
|
doc.Add(new TextField(LanguageTagField, code, Field.Store.NO)); |
|
} |
|
|
|
var englishNames = new System.Collections.Generic.HashSet<string>(); |
|
foreach (string code in await searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes)) |
|
{ |
|
Option<CultureInfo> maybeCultureInfo = _cultureInfos.Find( |
|
ci => string.Equals(ci.ThreeLetterISOLanguageName, code, StringComparison.OrdinalIgnoreCase)); |
|
foreach (CultureInfo cultureInfo in maybeCultureInfo) |
|
{ |
|
englishNames.Add(cultureInfo.EnglishName); |
|
} |
|
} |
|
|
|
foreach (string englishName in englishNames) |
|
{ |
|
doc.Add(new TextField(LanguageField, englishName, Field.Store.NO)); |
|
} |
|
} |
|
|
|
private async Task AddSubLanguages(ISearchRepository searchRepository, Document doc, List<string> mediaCodes) |
|
{ |
|
foreach (string code in mediaCodes.Where(c => !string.IsNullOrWhiteSpace(c)).Distinct()) |
|
{ |
|
doc.Add(new TextField(SubLanguageTagField, code, Field.Store.NO)); |
|
} |
|
|
|
var englishNames = new System.Collections.Generic.HashSet<string>(); |
|
foreach (string code in await searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes)) |
|
{ |
|
Option<CultureInfo> maybeCultureInfo = _cultureInfos.Find( |
|
ci => string.Equals(ci.ThreeLetterISOLanguageName, code, StringComparison.OrdinalIgnoreCase)); |
|
foreach (CultureInfo cultureInfo in maybeCultureInfo) |
|
{ |
|
englishNames.Add(cultureInfo.EnglishName); |
|
} |
|
} |
|
|
|
foreach (string englishName in englishNames) |
|
{ |
|
doc.Add(new TextField(SubLanguageField, englishName, Field.Store.NO)); |
|
} |
|
} |
|
|
|
private async Task UpdateShow(ISearchRepository searchRepository, Show show) |
|
{ |
|
Option<ShowMetadata> maybeMetadata = show.ShowMetadata.HeadOrNone(); |
|
if (maybeMetadata.IsSome) |
|
{ |
|
ShowMetadata metadata = maybeMetadata.ValueUnsafe(); |
|
|
|
try |
|
{ |
|
var doc = new Document |
|
{ |
|
new StringField(IdField, show.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES), |
|
new StringField(TypeField, ShowType, Field.Store.YES), |
|
new TextField(TitleField, metadata.Title, Field.Store.NO), |
|
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO), |
|
new TextField(LibraryNameField, show.LibraryPath.Library.Name, Field.Store.NO), |
|
new StringField( |
|
LibraryIdField, |
|
show.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture), |
|
Field.Store.NO), |
|
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO), |
|
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES), |
|
new StringField(StateField, show.State.ToString(), Field.Store.NO), |
|
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) |
|
}; |
|
|
|
List<string> languages = await searchRepository.GetLanguagesForShow(show); |
|
await AddLanguages(searchRepository, doc, languages); |
|
|
|
List<string> subLanguages = await searchRepository.GetSubLanguagesForShow(show); |
|
await AddSubLanguages(searchRepository, doc, subLanguages); |
|
|
|
if (!string.IsNullOrWhiteSpace(metadata.ContentRating)) |
|
{ |
|
foreach (string contentRating in (metadata.ContentRating ?? string.Empty).Split("/") |
|
.Map(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x))) |
|
{ |
|
doc.Add(new StringField(ContentRatingField, contentRating, Field.Store.NO)); |
|
} |
|
} |
|
|
|
if (metadata.ReleaseDate.HasValue) |
|
{ |
|
doc.Add( |
|
new StringField( |
|
ReleaseDateField, |
|
metadata.ReleaseDate.Value.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
} |
|
|
|
doc.Add( |
|
new StringField( |
|
AddedDateField, |
|
metadata.DateAdded.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
|
|
if (!string.IsNullOrWhiteSpace(metadata.Plot)) |
|
{ |
|
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO)); |
|
} |
|
|
|
foreach (Genre genre in metadata.Genres) |
|
{ |
|
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Tag tag in metadata.Tags) |
|
{ |
|
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO)); |
|
doc.Add(new StringField(TagFullField, tag.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Studio studio in metadata.Studios) |
|
{ |
|
doc.Add(new TextField(StudioField, studio.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Actor actor in metadata.Actors) |
|
{ |
|
doc.Add(new TextField(ActorField, actor.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (TraktListItem item in show.TraktListItems) |
|
{ |
|
doc.Add( |
|
new StringField( |
|
TraktListField, |
|
item.TraktList.TraktId.ToString(CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
} |
|
|
|
AddMetadataGuids(metadata, doc); |
|
|
|
_writer.UpdateDocument(new Term(IdField, show.Id.ToString(CultureInfo.InvariantCulture)), doc); |
|
} |
|
catch (Exception ex) |
|
{ |
|
metadata.Show = null; |
|
_logger.LogWarning(ex, "Error indexing show with metadata {@Metadata}", metadata); |
|
} |
|
} |
|
} |
|
|
|
private async Task UpdateSeason(ISearchRepository searchRepository, Season season) |
|
{ |
|
Option<SeasonMetadata> maybeMetadata = season.SeasonMetadata.HeadOrNone(); |
|
Option<ShowMetadata> maybeShowMetadata = season.Show.ShowMetadata.HeadOrNone(); |
|
if (maybeMetadata.IsSome && maybeShowMetadata.IsSome) |
|
{ |
|
SeasonMetadata metadata = maybeMetadata.ValueUnsafe(); |
|
ShowMetadata showMetadata = maybeShowMetadata.ValueUnsafe(); |
|
|
|
try |
|
{ |
|
var seasonTitle = $"{showMetadata.Title} - S{season.SeasonNumber}"; |
|
string sortTitle = $"{showMetadata.SortTitle}_{season.SeasonNumber:0000}" |
|
.ToLowerInvariant(); |
|
string titleAndYear = $"{showMetadata.Title}_{showMetadata.Year}_{season.SeasonNumber}" |
|
.ToLowerInvariant(); |
|
|
|
var doc = new Document |
|
{ |
|
new StringField(IdField, season.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES), |
|
new StringField(TypeField, SeasonType, Field.Store.YES), |
|
new TextField(TitleField, seasonTitle, Field.Store.NO), |
|
new StringField(SortTitleField, sortTitle, Field.Store.NO), |
|
new TextField(LibraryNameField, season.LibraryPath.Library.Name, Field.Store.NO), |
|
new StringField( |
|
LibraryIdField, |
|
season.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture), |
|
Field.Store.NO), |
|
new StringField(TitleAndYearField, titleAndYear, Field.Store.NO), |
|
new StringField(JumpLetterField, GetJumpLetter(showMetadata), Field.Store.YES), |
|
new StringField(StateField, season.State.ToString(), Field.Store.NO), |
|
new Int32Field(SeasonNumberField, season.SeasonNumber, Field.Store.NO), |
|
new TextField(ShowTitleField, showMetadata.Title, Field.Store.NO) |
|
}; |
|
|
|
// add some show fields to help filter seasons within a particular show |
|
foreach (Genre genre in showMetadata.Genres) |
|
{ |
|
doc.Add(new TextField(ShowGenreField, genre.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Tag tag in showMetadata.Tags) |
|
{ |
|
doc.Add(new TextField(ShowTagField, tag.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Studio studio in showMetadata.Studios) |
|
{ |
|
doc.Add(new TextField(ShowStudioField, studio.Name, Field.Store.NO)); |
|
} |
|
|
|
if (!string.IsNullOrWhiteSpace(showMetadata.ContentRating)) |
|
{ |
|
foreach (string contentRating in (showMetadata.ContentRating ?? string.Empty).Split("/") |
|
.Map(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x))) |
|
{ |
|
doc.Add(new StringField(ShowContentRatingField, contentRating, Field.Store.NO)); |
|
} |
|
} |
|
|
|
List<string> languages = await searchRepository.GetLanguagesForSeason(season); |
|
await AddLanguages(searchRepository, doc, languages); |
|
|
|
List<string> subLanguages = await searchRepository.GetSubLanguagesForSeason(season); |
|
await AddSubLanguages(searchRepository, doc, subLanguages); |
|
|
|
if (!string.IsNullOrWhiteSpace(showMetadata.ContentRating)) |
|
{ |
|
foreach (string contentRating in (showMetadata.ContentRating ?? string.Empty).Split("/") |
|
.Map(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x))) |
|
{ |
|
doc.Add(new StringField(ContentRatingField, contentRating, Field.Store.NO)); |
|
} |
|
} |
|
|
|
if (metadata.ReleaseDate.HasValue) |
|
{ |
|
doc.Add( |
|
new StringField( |
|
ReleaseDateField, |
|
metadata.ReleaseDate.Value.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
} |
|
|
|
doc.Add( |
|
new StringField( |
|
AddedDateField, |
|
metadata.DateAdded.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
|
|
foreach (TraktListItem item in season.TraktListItems) |
|
{ |
|
doc.Add( |
|
new StringField( |
|
TraktListField, |
|
item.TraktList.TraktId.ToString(CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
} |
|
|
|
foreach (Tag tag in metadata.Tags) |
|
{ |
|
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO)); |
|
doc.Add(new StringField(TagFullField, tag.Name, Field.Store.NO)); |
|
} |
|
|
|
AddMetadataGuids(metadata, doc); |
|
|
|
_writer.UpdateDocument(new Term(IdField, season.Id.ToString(CultureInfo.InvariantCulture)), doc); |
|
} |
|
catch (Exception ex) |
|
{ |
|
metadata.Season = null; |
|
_logger.LogWarning(ex, "Error indexing season with metadata {@Metadata}", metadata); |
|
} |
|
} |
|
} |
|
|
|
private async Task UpdateArtist(ISearchRepository searchRepository, Artist artist) |
|
{ |
|
Option<ArtistMetadata> maybeMetadata = artist.ArtistMetadata.HeadOrNone(); |
|
if (maybeMetadata.IsSome) |
|
{ |
|
ArtistMetadata metadata = maybeMetadata.ValueUnsafe(); |
|
|
|
try |
|
{ |
|
var doc = new Document |
|
{ |
|
new StringField(IdField, artist.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES), |
|
new StringField(TypeField, ArtistType, Field.Store.YES), |
|
new TextField(TitleField, metadata.Title, Field.Store.NO), |
|
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO), |
|
new TextField(LibraryNameField, artist.LibraryPath.Library.Name, Field.Store.NO), |
|
new StringField( |
|
LibraryIdField, |
|
artist.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture), |
|
Field.Store.NO), |
|
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO), |
|
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES), |
|
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) |
|
}; |
|
|
|
List<string> languages = await searchRepository.GetLanguagesForArtist(artist); |
|
await AddLanguages(searchRepository, doc, languages); |
|
|
|
List<string> subLanguages = await searchRepository.GetSubLanguagesForArtist(artist); |
|
await AddSubLanguages(searchRepository, doc, subLanguages); |
|
|
|
doc.Add( |
|
new StringField( |
|
AddedDateField, |
|
metadata.DateAdded.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
|
|
foreach (Genre genre in metadata.Genres) |
|
{ |
|
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Style style in metadata.Styles) |
|
{ |
|
doc.Add(new TextField(StyleField, style.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Mood mood in metadata.Moods) |
|
{ |
|
doc.Add(new TextField(MoodField, mood.Name, Field.Store.NO)); |
|
} |
|
|
|
AddMetadataGuids(metadata, doc); |
|
|
|
_writer.UpdateDocument(new Term(IdField, artist.Id.ToString(CultureInfo.InvariantCulture)), doc); |
|
} |
|
catch (Exception ex) |
|
{ |
|
metadata.Artist = null; |
|
_logger.LogWarning(ex, "Error indexing artist with metadata {@Metadata}", metadata); |
|
} |
|
} |
|
} |
|
|
|
private async Task UpdateMusicVideo(ISearchRepository searchRepository, MusicVideo musicVideo) |
|
{ |
|
Option<MusicVideoMetadata> maybeMetadata = musicVideo.MusicVideoMetadata.HeadOrNone(); |
|
if (maybeMetadata.IsSome) |
|
{ |
|
MusicVideoMetadata metadata = maybeMetadata.ValueUnsafe(); |
|
|
|
try |
|
{ |
|
var doc = new Document |
|
{ |
|
new StringField(IdField, musicVideo.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES), |
|
new StringField(TypeField, MusicVideoType, Field.Store.YES), |
|
new TextField(TitleField, metadata.Title ?? string.Empty, Field.Store.NO), |
|
new StringField( |
|
SortTitleField, |
|
(metadata.SortTitle ?? string.Empty).ToLowerInvariant(), |
|
Field.Store.NO), |
|
new TextField(LibraryNameField, musicVideo.LibraryPath.Library.Name, Field.Store.NO), |
|
new StringField( |
|
LibraryIdField, |
|
musicVideo.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture), |
|
Field.Store.NO), |
|
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO), |
|
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES), |
|
new StringField(StateField, musicVideo.State.ToString(), Field.Store.NO), |
|
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) |
|
}; |
|
|
|
await AddLanguages(searchRepository, doc, musicVideo.MediaVersions); |
|
|
|
AddStatistics(doc, musicVideo.MediaVersions); |
|
AddCollections(doc, musicVideo.Collections); |
|
|
|
if (metadata.ReleaseDate.HasValue) |
|
{ |
|
doc.Add( |
|
new StringField( |
|
ReleaseDateField, |
|
metadata.ReleaseDate.Value.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
} |
|
|
|
doc.Add( |
|
new StringField( |
|
AddedDateField, |
|
metadata.DateAdded.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
|
|
if (!string.IsNullOrWhiteSpace(metadata.Album)) |
|
{ |
|
doc.Add(new TextField(AlbumField, metadata.Album, Field.Store.NO)); |
|
} |
|
|
|
if (!string.IsNullOrWhiteSpace(metadata.Plot)) |
|
{ |
|
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO)); |
|
} |
|
|
|
foreach (Genre genre in metadata.Genres) |
|
{ |
|
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Tag tag in metadata.Tags) |
|
{ |
|
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO)); |
|
doc.Add(new StringField(TagFullField, tag.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Studio studio in metadata.Studios) |
|
{ |
|
doc.Add(new TextField(StudioField, studio.Name, Field.Store.NO)); |
|
} |
|
|
|
var artists = new System.Collections.Generic.HashSet<string>(); |
|
|
|
if (musicVideo.Artist != null) |
|
{ |
|
foreach (ArtistMetadata artistMetadata in musicVideo.Artist.ArtistMetadata) |
|
{ |
|
artists.Add(artistMetadata.Title); |
|
} |
|
} |
|
|
|
foreach (MusicVideoArtist artist in metadata.Artists) |
|
{ |
|
artists.Add(artist.Name); |
|
} |
|
|
|
foreach (string artist in artists) |
|
{ |
|
doc.Add(new TextField(ArtistField, artist, Field.Store.NO)); |
|
} |
|
|
|
AddMetadataGuids(metadata, doc); |
|
|
|
_writer.UpdateDocument(new Term(IdField, musicVideo.Id.ToString(CultureInfo.InvariantCulture)), doc); |
|
} |
|
catch (Exception ex) |
|
{ |
|
metadata.MusicVideo = null; |
|
_logger.LogWarning(ex, "Error indexing music video with metadata {@Metadata}", metadata); |
|
} |
|
} |
|
} |
|
|
|
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) |
|
{ |
|
try |
|
{ |
|
var doc = new Document |
|
{ |
|
new StringField(IdField, episode.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES), |
|
new StringField(TypeField, EpisodeType, Field.Store.YES), |
|
new TextField(LibraryNameField, episode.LibraryPath.Library.Name, Field.Store.NO), |
|
new StringField( |
|
LibraryIdField, |
|
episode.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture), |
|
Field.Store.NO), |
|
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO), |
|
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES), |
|
new StringField(StateField, episode.State.ToString(), Field.Store.NO), |
|
new Int32Field(SeasonNumberField, episode.Season?.SeasonNumber ?? 0, Field.Store.NO), |
|
new Int32Field(EpisodeNumberField, metadata.EpisodeNumber, Field.Store.NO), |
|
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) |
|
}; |
|
|
|
// add some show fields to help filter episodes within a particular show |
|
foreach (ShowMetadata showMetadata in Optional(episode.Season?.Show?.ShowMetadata).Flatten()) |
|
{ |
|
doc.Add(new TextField(ShowTitleField, showMetadata.Title, Field.Store.NO)); |
|
|
|
foreach (Genre genre in showMetadata.Genres) |
|
{ |
|
doc.Add(new TextField(ShowGenreField, genre.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Tag tag in showMetadata.Tags) |
|
{ |
|
doc.Add(new TextField(ShowTagField, tag.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Studio studio in showMetadata.Studios) |
|
{ |
|
doc.Add(new TextField(ShowStudioField, studio.Name, Field.Store.NO)); |
|
} |
|
|
|
if (!string.IsNullOrWhiteSpace(showMetadata.ContentRating)) |
|
{ |
|
foreach (string contentRating in (showMetadata.ContentRating ?? string.Empty).Split("/") |
|
.Map(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x))) |
|
{ |
|
doc.Add(new StringField(ShowContentRatingField, contentRating, Field.Store.NO)); |
|
} |
|
} |
|
} |
|
|
|
if (!string.IsNullOrWhiteSpace(metadata.Title)) |
|
{ |
|
doc.Add(new TextField(TitleField, metadata.Title, Field.Store.NO)); |
|
} |
|
|
|
if (!string.IsNullOrWhiteSpace(metadata.SortTitle)) |
|
{ |
|
doc.Add(new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO)); |
|
} |
|
|
|
await AddLanguages(searchRepository, doc, episode.MediaVersions); |
|
|
|
AddStatistics(doc, episode.MediaVersions); |
|
AddCollections(doc, episode.Collections); |
|
|
|
if (metadata.ReleaseDate.HasValue) |
|
{ |
|
doc.Add( |
|
new StringField( |
|
ReleaseDateField, |
|
metadata.ReleaseDate.Value.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
} |
|
|
|
doc.Add( |
|
new StringField( |
|
AddedDateField, |
|
metadata.DateAdded.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
|
|
if (!string.IsNullOrWhiteSpace(metadata.Plot)) |
|
{ |
|
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO)); |
|
} |
|
|
|
foreach (Genre genre in metadata.Genres) |
|
{ |
|
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Tag tag in metadata.Tags) |
|
{ |
|
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO)); |
|
doc.Add(new StringField(TagFullField, tag.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Studio studio in metadata.Studios) |
|
{ |
|
doc.Add(new TextField(StudioField, studio.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Actor actor in metadata.Actors) |
|
{ |
|
doc.Add(new TextField(ActorField, actor.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Director director in metadata.Directors) |
|
{ |
|
doc.Add(new TextField(DirectorField, director.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Writer writer in metadata.Writers) |
|
{ |
|
doc.Add(new TextField(WriterField, writer.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (TraktListItem item in episode.TraktListItems) |
|
{ |
|
doc.Add( |
|
new StringField( |
|
TraktListField, |
|
item.TraktList.TraktId.ToString(CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
} |
|
|
|
AddMetadataGuids(metadata, doc); |
|
|
|
_writer.UpdateDocument(new Term(IdField, episode.Id.ToString(CultureInfo.InvariantCulture)), doc); |
|
} |
|
catch (Exception ex) |
|
{ |
|
metadata.Episode = null; |
|
_logger.LogWarning(ex, "Error indexing episode with metadata {@Metadata}", metadata); |
|
} |
|
} |
|
} |
|
|
|
private async Task UpdateOtherVideo(ISearchRepository searchRepository, OtherVideo otherVideo) |
|
{ |
|
Option<OtherVideoMetadata> maybeMetadata = otherVideo.OtherVideoMetadata.HeadOrNone(); |
|
if (maybeMetadata.IsSome) |
|
{ |
|
OtherVideoMetadata metadata = maybeMetadata.ValueUnsafe(); |
|
|
|
try |
|
{ |
|
var doc = new Document |
|
{ |
|
new StringField(IdField, otherVideo.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES), |
|
new StringField(TypeField, OtherVideoType, Field.Store.YES), |
|
new TextField(TitleField, metadata.Title, Field.Store.NO), |
|
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO), |
|
new TextField(LibraryNameField, otherVideo.LibraryPath.Library.Name, Field.Store.NO), |
|
new StringField( |
|
LibraryIdField, |
|
otherVideo.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture), |
|
Field.Store.NO), |
|
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO), |
|
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES), |
|
new StringField(StateField, otherVideo.State.ToString(), Field.Store.NO), |
|
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) |
|
}; |
|
|
|
await AddLanguages(searchRepository, doc, otherVideo.MediaVersions); |
|
|
|
AddStatistics(doc, otherVideo.MediaVersions); |
|
AddCollections(doc, otherVideo.Collections); |
|
|
|
if (!string.IsNullOrWhiteSpace(metadata.ContentRating)) |
|
{ |
|
foreach (string contentRating in (metadata.ContentRating ?? string.Empty).Split("/") |
|
.Map(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x))) |
|
{ |
|
doc.Add(new StringField(ContentRatingField, contentRating, Field.Store.NO)); |
|
} |
|
} |
|
|
|
if (metadata.ReleaseDate.HasValue) |
|
{ |
|
doc.Add( |
|
new StringField( |
|
ReleaseDateField, |
|
metadata.ReleaseDate.Value.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
} |
|
|
|
doc.Add( |
|
new StringField( |
|
AddedDateField, |
|
metadata.DateAdded.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
|
|
if (!string.IsNullOrWhiteSpace(metadata.Plot)) |
|
{ |
|
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO)); |
|
} |
|
|
|
foreach (Genre genre in metadata.Genres) |
|
{ |
|
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Tag tag in metadata.Tags) |
|
{ |
|
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO)); |
|
doc.Add(new StringField(TagFullField, tag.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Studio studio in metadata.Studios) |
|
{ |
|
doc.Add(new TextField(StudioField, studio.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Actor actor in metadata.Actors) |
|
{ |
|
doc.Add(new TextField(ActorField, actor.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Director director in metadata.Directors) |
|
{ |
|
doc.Add(new TextField(DirectorField, director.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Writer writer in metadata.Writers) |
|
{ |
|
doc.Add(new TextField(WriterField, writer.Name, Field.Store.NO)); |
|
} |
|
|
|
AddMetadataGuids(metadata, doc); |
|
|
|
_writer.UpdateDocument(new Term(IdField, otherVideo.Id.ToString(CultureInfo.InvariantCulture)), doc); |
|
} |
|
catch (Exception ex) |
|
{ |
|
metadata.OtherVideo = null; |
|
_logger.LogWarning(ex, "Error indexing other video with metadata {@Metadata}", metadata); |
|
} |
|
} |
|
} |
|
|
|
private async Task UpdateSong(ISearchRepository searchRepository, Song song) |
|
{ |
|
Option<SongMetadata> maybeMetadata = song.SongMetadata.HeadOrNone(); |
|
if (maybeMetadata.IsSome) |
|
{ |
|
SongMetadata metadata = maybeMetadata.ValueUnsafe(); |
|
|
|
try |
|
{ |
|
var doc = new Document |
|
{ |
|
new StringField(IdField, song.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES), |
|
new StringField(TypeField, SongType, Field.Store.YES), |
|
new TextField(TitleField, metadata.Title, Field.Store.NO), |
|
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO), |
|
new TextField(LibraryNameField, song.LibraryPath.Library.Name, Field.Store.NO), |
|
new StringField( |
|
LibraryIdField, |
|
song.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture), |
|
Field.Store.NO), |
|
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO), |
|
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES), |
|
new StringField(StateField, song.State.ToString(), Field.Store.NO), |
|
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) |
|
}; |
|
|
|
await AddLanguages(searchRepository, doc, song.MediaVersions); |
|
|
|
AddStatistics(doc, song.MediaVersions); |
|
AddCollections(doc, song.Collections); |
|
|
|
doc.Add( |
|
new StringField( |
|
AddedDateField, |
|
metadata.DateAdded.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
|
|
if (!string.IsNullOrWhiteSpace(metadata.Album)) |
|
{ |
|
doc.Add(new TextField(AlbumField, metadata.Album, Field.Store.NO)); |
|
} |
|
|
|
foreach (string artist in metadata.Artists) |
|
{ |
|
doc.Add(new TextField(ArtistField, artist, Field.Store.NO)); |
|
} |
|
|
|
foreach (string albumArtist in metadata.AlbumArtists) |
|
{ |
|
doc.Add(new TextField(AlbumArtistField, albumArtist, Field.Store.NO)); |
|
} |
|
|
|
foreach (Tag tag in metadata.Tags) |
|
{ |
|
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO)); |
|
doc.Add(new StringField(TagFullField, tag.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Genre genre in metadata.Genres) |
|
{ |
|
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO)); |
|
} |
|
|
|
AddMetadataGuids(metadata, doc); |
|
|
|
_writer.UpdateDocument(new Term(IdField, song.Id.ToString(CultureInfo.InvariantCulture)), doc); |
|
} |
|
catch (Exception ex) |
|
{ |
|
metadata.Song = null; |
|
_logger.LogWarning(ex, "Error indexing song with metadata {@Metadata}", metadata); |
|
} |
|
} |
|
} |
|
|
|
private async Task UpdateImage(ISearchRepository searchRepository, Image image) |
|
{ |
|
Option<ImageMetadata> maybeMetadata = image.ImageMetadata.HeadOrNone(); |
|
if (maybeMetadata.IsSome) |
|
{ |
|
ImageMetadata metadata = maybeMetadata.ValueUnsafe(); |
|
|
|
try |
|
{ |
|
var doc = new Document |
|
{ |
|
new StringField(IdField, image.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES), |
|
new StringField(TypeField, ImageType, Field.Store.YES), |
|
new TextField(TitleField, metadata.Title, Field.Store.NO), |
|
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO), |
|
new TextField(LibraryNameField, image.LibraryPath.Library.Name, Field.Store.NO), |
|
new StringField( |
|
LibraryIdField, |
|
image.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture), |
|
Field.Store.NO), |
|
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO), |
|
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES), |
|
new StringField(StateField, image.State.ToString(), Field.Store.NO), |
|
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO) |
|
}; |
|
|
|
IEnumerable<int> libraryFolderIds = image.MediaVersions |
|
.SelectMany(mv => mv.MediaFiles) |
|
.SelectMany(mf => Optional(mf.LibraryFolderId)); |
|
|
|
foreach (int libraryFolderId in libraryFolderIds) |
|
{ |
|
doc.Add( |
|
new StringField( |
|
LibraryFolderIdField, |
|
libraryFolderId.ToString(CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
} |
|
|
|
await AddLanguages(searchRepository, doc, image.MediaVersions); |
|
|
|
AddStatistics(doc, image.MediaVersions); |
|
AddCollections(doc, image.Collections); |
|
|
|
doc.Add( |
|
new StringField( |
|
AddedDateField, |
|
metadata.DateAdded.ToString("yyyyMMdd", CultureInfo.InvariantCulture), |
|
Field.Store.NO)); |
|
|
|
foreach (Tag tag in metadata.Tags) |
|
{ |
|
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO)); |
|
doc.Add(new StringField(TagFullField, tag.Name, Field.Store.NO)); |
|
} |
|
|
|
foreach (Genre genre in metadata.Genres) |
|
{ |
|
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO)); |
|
} |
|
|
|
AddMetadataGuids(metadata, doc); |
|
|
|
_writer.UpdateDocument(new Term(IdField, image.Id.ToString(CultureInfo.InvariantCulture)), doc); |
|
} |
|
catch (Exception ex) |
|
{ |
|
metadata.Image = null; |
|
_logger.LogWarning(ex, "Error indexing image with metadata {@Metadata}", metadata); |
|
} |
|
} |
|
} |
|
|
|
private static SearchItem ProjectToSearchItem(Document doc) => new( |
|
doc.Get(TypeField, CultureInfo.InvariantCulture), |
|
Convert.ToInt32(doc.Get(IdField, CultureInfo.InvariantCulture), CultureInfo.InvariantCulture)); |
|
|
|
private static void AddStatistics(Document doc, List<MediaVersion> mediaVersions) |
|
{ |
|
foreach (MediaVersion version in mediaVersions) |
|
{ |
|
doc.Add(new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO)); |
|
doc.Add(new Int32Field(SecondsField, (int)Math.Ceiling(version.Duration.TotalSeconds), Field.Store.NO)); |
|
|
|
if (version.Streams.Any(s => s.MediaStreamKind == MediaStreamKind.Video)) |
|
{ |
|
doc.Add(new Int32Field(HeightField, version.Height, Field.Store.NO)); |
|
doc.Add(new Int32Field(WidthField, version.Width, Field.Store.NO)); |
|
} |
|
|
|
foreach (MediaStream videoStream in version.Streams.Filter(s => s.MediaStreamKind == MediaStreamKind.Video)) |
|
{ |
|
if (!string.IsNullOrWhiteSpace(videoStream.Codec)) |
|
{ |
|
doc.Add(new StringField(VideoCodecField, videoStream.Codec, Field.Store.NO)); |
|
} |
|
|
|
Option<IPixelFormat> maybePixelFormat = |
|
AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, null); |
|
foreach (IPixelFormat pixelFormat in maybePixelFormat) |
|
{ |
|
doc.Add(new Int32Field(VideoBitDepthField, pixelFormat.BitDepth, Field.Store.NO)); |
|
} |
|
|
|
if (maybePixelFormat.IsNone && videoStream.BitsPerRawSample > 0) |
|
{ |
|
doc.Add(new Int32Field(VideoBitDepthField, videoStream.BitsPerRawSample, Field.Store.NO)); |
|
} |
|
|
|
var colorParams = new ColorParams( |
|
videoStream.ColorRange, |
|
videoStream.ColorSpace, |
|
videoStream.ColorTransfer, |
|
videoStream.ColorPrimaries); |
|
|
|
string dynamicRange = colorParams.IsHdr ? "hdr" : "sdr"; |
|
|
|
doc.Add(new StringField(VideoDynamicRangeField, dynamicRange, Field.Store.NO)); |
|
} |
|
} |
|
} |
|
|
|
private static void AddCollections(Document doc, List<Collection> collections) |
|
{ |
|
foreach (Collection collection in collections) |
|
{ |
|
doc.Add(new StringField(CollectionField, collection.Name.ToLowerInvariant(), Field.Store.NO)); |
|
} |
|
} |
|
|
|
private static void AddMetadataGuids(Core.Domain.Metadata metadata, Document doc) |
|
{ |
|
foreach (MetadataGuid guid in metadata.Guids) |
|
{ |
|
string[] split = (guid.Guid ?? string.Empty).Split("://"); |
|
if (split.Length == 2 && !string.IsNullOrWhiteSpace(split[1])) |
|
{ |
|
doc.Add(new StringField(split[0], split[1].ToLowerInvariant(), Field.Store.NO)); |
|
} |
|
} |
|
} |
|
|
|
// this is used for filtering duplicate search results |
|
internal static string GetTitleAndYear(Core.Domain.Metadata metadata) => |
|
metadata switch |
|
{ |
|
EpisodeMetadata em => |
|
$"{Title(em)}_{em.Episode.Season.Show.ShowMetadata.Head().Title}_{em.Year}_{em.Episode.Season.SeasonNumber}_{em.EpisodeNumber}_{em.Episode.State}" |
|
.ToLowerInvariant(), |
|
OtherVideoMetadata ovm => $"{OtherVideoTitle(ovm).Replace(' ', '_')}_{ovm.Year}_{ovm.OtherVideo.State}" |
|
.ToLowerInvariant(), |
|
SongMetadata sm => $"{Title(sm)}_{sm.Year}_{sm.Song.State}".ToLowerInvariant(), |
|
ImageMetadata im => $"{Title(im)}_{im.Year}_{im.Image.State}".ToLowerInvariant(), |
|
MovieMetadata mm => $"{Title(mm)}_{mm.Year}_{mm.Movie.State}".ToLowerInvariant(), |
|
ArtistMetadata am => $"{Title(am)}_{am.Year}_{am.Artist.State}".ToLowerInvariant(), |
|
MusicVideoMetadata mvm => $"{Title(mvm)}_{mvm.Year}_{mvm.MusicVideo.State}".ToLowerInvariant(), |
|
SeasonMetadata sm => $"{Title(sm)}_{sm.Year}_{sm.Season.State}".ToLowerInvariant(), |
|
ShowMetadata sm => $"{Title(sm)}_{sm.Year}_{sm.Show.State}".ToLowerInvariant(), |
|
_ => $"{Title(metadata)}_{metadata.Year}".ToLowerInvariant() |
|
}; |
|
|
|
private static string Title(Core.Domain.Metadata metadata) => |
|
(metadata.Title ?? string.Empty).Replace(' ', '_'); |
|
|
|
internal static string GetJumpLetter(Core.Domain.Metadata metadata) |
|
{ |
|
foreach (char c in (metadata.SortTitle ?? " ").ToLowerInvariant().HeadOrNone()) |
|
{ |
|
return c switch |
|
{ |
|
>= 'a' and <= 'z' => c.ToString(), |
|
_ => "#" |
|
}; |
|
} |
|
|
|
return "#"; |
|
} |
|
|
|
private static string OtherVideoTitle(OtherVideoMetadata ovm) => |
|
string.IsNullOrWhiteSpace(ovm.OriginalTitle) ? ovm.Title : ovm.OriginalTitle; |
|
}
|
|
|