mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* wip * first pass at elasticsearch; movies kind of work * use field name constants * properly sort search results * fix some crashes * fix page map/jump letters * optimize page map using terms aggregation * index all item types * optionally use elastic search * code cleanup * automatically rebuild lucene index after improper shutdown * update changelogpull/1371/head
34 changed files with 1168 additions and 139 deletions
@ -0,0 +1,828 @@ |
|||||||
|
using System.Globalization; |
||||||
|
using Bugsnag; |
||||||
|
using Elastic.Clients.Elasticsearch; |
||||||
|
using Elastic.Clients.Elasticsearch.Aggregations; |
||||||
|
using Elastic.Clients.Elasticsearch.IndexManagement; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Metadata; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching; |
||||||
|
using ErsatzTV.Core.Interfaces.Search; |
||||||
|
using ErsatzTV.Core.Search; |
||||||
|
using ErsatzTV.FFmpeg; |
||||||
|
using ErsatzTV.FFmpeg.Format; |
||||||
|
using ErsatzTV.Infrastructure.Search.Models; |
||||||
|
using Microsoft.Extensions.Logging; |
||||||
|
using ExistsResponse = Elastic.Clients.Elasticsearch.IndexManagement.ExistsResponse; |
||||||
|
using MediaStream = ErsatzTV.Core.Domain.MediaStream; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Search; |
||||||
|
|
||||||
|
public class ElasticSearchIndex : ISearchIndex |
||||||
|
{ |
||||||
|
public static Uri Uri; |
||||||
|
public static string IndexName; |
||||||
|
private readonly List<CultureInfo> _cultureInfos; |
||||||
|
|
||||||
|
private readonly ILogger<ElasticSearchIndex> _logger; |
||||||
|
private ElasticsearchClient _client; |
||||||
|
|
||||||
|
public ElasticSearchIndex(ILogger<ElasticSearchIndex> logger) |
||||||
|
{ |
||||||
|
_logger = logger; |
||||||
|
_cultureInfos = CultureInfo.GetCultures(CultureTypes.NeutralCultures).ToList(); |
||||||
|
} |
||||||
|
|
||||||
|
public void Dispose() |
||||||
|
{ |
||||||
|
// do nothing
|
||||||
|
} |
||||||
|
|
||||||
|
// not really used by elasticsearch
|
||||||
|
public Task<bool> IndexExists() => Task.FromResult(false); |
||||||
|
|
||||||
|
public int Version => 36; |
||||||
|
|
||||||
|
public async Task<bool> Initialize( |
||||||
|
ILocalFileSystem localFileSystem, |
||||||
|
IConfigElementRepository configElementRepository) |
||||||
|
{ |
||||||
|
_client = new ElasticsearchClient(Uri); |
||||||
|
ExistsResponse exists = await _client.Indices.ExistsAsync(IndexName); |
||||||
|
if (!exists.IsValidResponse) |
||||||
|
{ |
||||||
|
CreateIndexResponse createResponse = await CreateIndex(); |
||||||
|
return createResponse.IsValidResponse; |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<Unit> Rebuild( |
||||||
|
ICachingSearchRepository searchRepository, |
||||||
|
IFallbackMetadataProvider fallbackMetadataProvider) |
||||||
|
{ |
||||||
|
DeleteIndexResponse deleteResponse = await _client.Indices.DeleteAsync(IndexName); |
||||||
|
if (!deleteResponse.IsValidResponse) |
||||||
|
{ |
||||||
|
return Unit.Default; |
||||||
|
} |
||||||
|
|
||||||
|
CreateIndexResponse createResponse = await CreateIndex(); |
||||||
|
if (!createResponse.IsValidResponse) |
||||||
|
{ |
||||||
|
return Unit.Default; |
||||||
|
} |
||||||
|
|
||||||
|
await foreach (MediaItem mediaItem in searchRepository.GetAllMediaItems()) |
||||||
|
{ |
||||||
|
await RebuildItem(searchRepository, fallbackMetadataProvider, mediaItem); |
||||||
|
} |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return Unit.Default; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<Unit> RemoveItems(IEnumerable<int> ids) |
||||||
|
{ |
||||||
|
await _client.BulkAsync( |
||||||
|
descriptor => descriptor |
||||||
|
.Index(IndexName) |
||||||
|
.DeleteMany(ids.Map(id => new Id(id))) |
||||||
|
); |
||||||
|
|
||||||
|
return Unit.Default; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<SearchResult> Search(IClient client, string query, int skip, int limit, string searchField = "") |
||||||
|
{ |
||||||
|
var items = new List<MinimalElasticSearchItem>(); |
||||||
|
var totalCount = 0; |
||||||
|
|
||||||
|
SearchResponse<MinimalElasticSearchItem> response = await _client.SearchAsync<MinimalElasticSearchItem>( |
||||||
|
s => s.Index(IndexName) |
||||||
|
.Sort(ss => ss.Field(f => f.SortTitle, fs => fs.Order(SortOrder.Asc))) |
||||||
|
.From(skip) |
||||||
|
.Size(limit) |
||||||
|
.QueryLuceneSyntax(query)); |
||||||
|
if (response.IsValidResponse) |
||||||
|
{ |
||||||
|
items.AddRange(response.Documents); |
||||||
|
totalCount = (int)response.Total; |
||||||
|
} |
||||||
|
|
||||||
|
var searchResult = new SearchResult(items.Map(i => new SearchItem(i.Type, i.Id)).ToList(), totalCount); |
||||||
|
|
||||||
|
if (limit > 0) |
||||||
|
{ |
||||||
|
searchResult.PageMap = await GetSearchPageMap(query, limit); |
||||||
|
} |
||||||
|
|
||||||
|
return searchResult; |
||||||
|
} |
||||||
|
|
||||||
|
public void Commit() |
||||||
|
{ |
||||||
|
// do nothing
|
||||||
|
} |
||||||
|
|
||||||
|
private async Task<CreateIndexResponse> CreateIndex() => |
||||||
|
await _client.Indices.CreateAsync<ElasticSearchItem>( |
||||||
|
IndexName, |
||||||
|
i => i.Mappings( |
||||||
|
m => m.Properties( |
||||||
|
p => p |
||||||
|
.Keyword(t => t.Type, t => t.Store()) |
||||||
|
.Text(t => t.Title, t => t.Store(false)) |
||||||
|
.Keyword(t => t.SortTitle, t => t.Store(false)) |
||||||
|
.Text(t => t.LibraryName, t => t.Store(false)) |
||||||
|
.Keyword(t => t.LibraryId, t => t.Store(false)) |
||||||
|
.Keyword(t => t.TitleAndYear, t => t.Store(false)) |
||||||
|
.Keyword(t => t.JumpLetter, t => t.Store()) |
||||||
|
.Keyword(t => t.State, t => t.Store(false)) |
||||||
|
.Text(t => t.MetadataKind, t => t.Store(false)) |
||||||
|
.Text(t => t.Language, t => t.Store(false)) |
||||||
|
.IntegerNumber(t => t.Height, t => t.Store(false)) |
||||||
|
.IntegerNumber(t => t.Width, t => t.Store(false)) |
||||||
|
.Keyword(t => t.VideoCodec, t => t.Store(false)) |
||||||
|
.IntegerNumber(t => t.VideoBitDepth, t => t.Store(false)) |
||||||
|
.Keyword(t => t.VideoDynamicRange, t => t.Store(false)) |
||||||
|
.Keyword(t => t.ContentRating, t => t.Store(false)) |
||||||
|
.Keyword(t => t.ReleaseDate, t => t.Store(false)) |
||||||
|
.Keyword(t => t.AddedDate, t => t.Store(false)) |
||||||
|
.Text(t => t.Plot, t => t.Store(false)) |
||||||
|
.Text(t => t.Genre, t => t.Store(false)) |
||||||
|
.Text(t => t.Tag, t => t.Store(false)) |
||||||
|
.Text(t => t.Studio, t => t.Store(false)) |
||||||
|
.Text(t => t.Actor, t => t.Store(false)) |
||||||
|
.Text(t => t.Director, t => t.Store(false)) |
||||||
|
.Text(t => t.Writer, t => t.Store(false)) |
||||||
|
.Keyword(t => t.TraktList, t => t.Store(false)) |
||||||
|
.IntegerNumber(t => t.SeasonNumber, t => t.Store(false)) |
||||||
|
.Text(t => t.ShowTitle, t => t.Store(false)) |
||||||
|
.Text(t => t.ShowGenre, t => t.Store(false)) |
||||||
|
.Text(t => t.ShowTag, t => t.Store(false)) |
||||||
|
.Text(t => t.Style, t => t.Store(false)) |
||||||
|
.Text(t => t.Mood, t => t.Store(false)) |
||||||
|
.Text(t => t.Album, t => t.Store(false)) |
||||||
|
.Text(t => t.Artist, t => t.Store(false)) |
||||||
|
.IntegerNumber(t => t.EpisodeNumber, t => t.Store(false)) |
||||||
|
.Text(t => t.AlbumArtist, t => t.Store(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; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async Task UpdateMovie(ISearchRepository searchRepository, Movie movie) |
||||||
|
{ |
||||||
|
foreach (MovieMetadata metadata in movie.MovieMetadata.HeadOrNone()) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
var doc = new ElasticSearchItem |
||||||
|
{ |
||||||
|
Id = movie.Id, |
||||||
|
Type = LuceneSearchIndex.MovieType, |
||||||
|
Title = metadata.Title, |
||||||
|
SortTitle = metadata.SortTitle.ToLowerInvariant(), |
||||||
|
LibraryName = movie.LibraryPath.Library.Name, |
||||||
|
LibraryId = movie.LibraryPath.Library.Id, |
||||||
|
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata), |
||||||
|
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), |
||||||
|
State = movie.State.ToString(), |
||||||
|
MetadataKind = metadata.MetadataKind.ToString(), |
||||||
|
Language = await GetLanguages(searchRepository, movie.MediaVersions), |
||||||
|
ContentRating = GetContentRatings(metadata.ContentRating), |
||||||
|
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), |
||||||
|
AddedDate = GetAddedDate(metadata.DateAdded), |
||||||
|
Plot = metadata.Plot ?? string.Empty, |
||||||
|
Genre = metadata.Genres.Map(g => g.Name).ToList(), |
||||||
|
Tag = metadata.Tags.Map(t => t.Name).ToList(), |
||||||
|
Studio = metadata.Studios.Map(s => s.Name).ToList(), |
||||||
|
Actor = metadata.Actors.Map(a => a.Name).ToList(), |
||||||
|
Director = metadata.Directors.Map(d => d.Name).ToList(), |
||||||
|
Writer = metadata.Writers.Map(w => w.Name).ToList(), |
||||||
|
TraktList = movie.TraktListItems.Map(t => t.TraktList.TraktId.ToString()).ToList() |
||||||
|
}; |
||||||
|
|
||||||
|
AddStatistics(doc, movie.MediaVersions); |
||||||
|
|
||||||
|
foreach ((string key, List<string> value) in GetMetadataGuids(metadata)) |
||||||
|
{ |
||||||
|
doc.AdditionalProperties.Add(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
await _client.IndexAsync(doc, IndexName); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
metadata.Movie = null; |
||||||
|
_logger.LogWarning(ex, "Error indexing movie with metadata {@Metadata}", metadata); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async Task UpdateShow(ISearchRepository searchRepository, Show show) |
||||||
|
{ |
||||||
|
foreach (ShowMetadata metadata in show.ShowMetadata.HeadOrNone()) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
var doc = new ElasticSearchItem |
||||||
|
{ |
||||||
|
Id = show.Id, |
||||||
|
Type = LuceneSearchIndex.ShowType, |
||||||
|
Title = metadata.Title, |
||||||
|
SortTitle = metadata.SortTitle.ToLowerInvariant(), |
||||||
|
LibraryName = show.LibraryPath.Library.Name, |
||||||
|
LibraryId = show.LibraryPath.Library.Id, |
||||||
|
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata), |
||||||
|
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), |
||||||
|
State = show.State.ToString(), |
||||||
|
MetadataKind = metadata.MetadataKind.ToString(), |
||||||
|
Language = await GetLanguages(searchRepository, await searchRepository.GetLanguagesForShow(show)), |
||||||
|
ContentRating = GetContentRatings(metadata.ContentRating), |
||||||
|
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), |
||||||
|
AddedDate = GetAddedDate(metadata.DateAdded), |
||||||
|
Plot = metadata.Plot ?? string.Empty, |
||||||
|
Genre = metadata.Genres.Map(g => g.Name).ToList(), |
||||||
|
Tag = metadata.Tags.Map(t => t.Name).ToList(), |
||||||
|
Studio = metadata.Studios.Map(s => s.Name).ToList(), |
||||||
|
Actor = metadata.Actors.Map(a => a.Name).ToList(), |
||||||
|
TraktList = show.TraktListItems.Map(t => t.TraktList.TraktId.ToString()).ToList() |
||||||
|
}; |
||||||
|
|
||||||
|
foreach ((string key, List<string> value) in GetMetadataGuids(metadata)) |
||||||
|
{ |
||||||
|
doc.AdditionalProperties.Add(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
await _client.IndexAsync(doc, IndexName); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
metadata.Show = null; |
||||||
|
_logger.LogWarning(ex, "Error indexing show with metadata {@Metadata}", metadata); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async Task UpdateSeason(ISearchRepository searchRepository, Season season) |
||||||
|
{ |
||||||
|
foreach (SeasonMetadata metadata in season.SeasonMetadata.HeadOrNone()) |
||||||
|
foreach (ShowMetadata showMetadata in season.Show.ShowMetadata.HeadOrNone()) |
||||||
|
{ |
||||||
|
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 ElasticSearchItem |
||||||
|
{ |
||||||
|
Id = season.Id, |
||||||
|
Type = LuceneSearchIndex.SeasonType, |
||||||
|
Title = seasonTitle, |
||||||
|
SortTitle = sortTitle, |
||||||
|
LibraryName = season.LibraryPath.Library.Name, |
||||||
|
LibraryId = season.LibraryPath.Library.Id, |
||||||
|
TitleAndYear = titleAndYear, |
||||||
|
JumpLetter = LuceneSearchIndex.GetJumpLetter(showMetadata), |
||||||
|
State = season.State.ToString(), |
||||||
|
SeasonNumber = season.SeasonNumber, |
||||||
|
ShowTitle = showMetadata.Title, |
||||||
|
ShowGenre = showMetadata.Genres.Map(g => g.Name).ToList(), |
||||||
|
ShowTag = showMetadata.Tags.Map(t => t.Name).ToList(), |
||||||
|
Language = await GetLanguages( |
||||||
|
searchRepository, |
||||||
|
await searchRepository.GetLanguagesForSeason(season)), |
||||||
|
ContentRating = GetContentRatings(showMetadata.ContentRating), |
||||||
|
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), |
||||||
|
AddedDate = GetAddedDate(metadata.DateAdded), |
||||||
|
TraktList = season.TraktListItems.Map(t => t.TraktList.TraktId.ToString()).ToList(), |
||||||
|
Tag = metadata.Tags.Map(a => a.Name).ToList() |
||||||
|
}; |
||||||
|
|
||||||
|
foreach ((string key, List<string> value) in GetMetadataGuids(metadata)) |
||||||
|
{ |
||||||
|
doc.AdditionalProperties.Add(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
await _client.IndexAsync(doc, IndexName); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
metadata.Season = null; |
||||||
|
_logger.LogWarning(ex, "Error indexing season with metadata {@Metadata}", metadata); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async Task UpdateArtist(ISearchRepository searchRepository, Artist artist) |
||||||
|
{ |
||||||
|
foreach (ArtistMetadata metadata in artist.ArtistMetadata.HeadOrNone()) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
var doc = new ElasticSearchItem |
||||||
|
{ |
||||||
|
Id = artist.Id, |
||||||
|
Type = LuceneSearchIndex.ArtistType, |
||||||
|
Title = metadata.Title, |
||||||
|
SortTitle = metadata.SortTitle.ToLowerInvariant(), |
||||||
|
LibraryName = artist.LibraryPath.Library.Name, |
||||||
|
LibraryId = artist.LibraryPath.Library.Id, |
||||||
|
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata), |
||||||
|
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), |
||||||
|
State = artist.State.ToString(), |
||||||
|
MetadataKind = metadata.MetadataKind.ToString(), |
||||||
|
Language = await GetLanguages( |
||||||
|
searchRepository, |
||||||
|
await searchRepository.GetLanguagesForArtist(artist)), |
||||||
|
AddedDate = GetAddedDate(metadata.DateAdded), |
||||||
|
Genre = metadata.Genres.Map(g => g.Name).ToList(), |
||||||
|
Style = metadata.Styles.Map(t => t.Name).ToList(), |
||||||
|
Mood = metadata.Moods.Map(s => s.Name).ToList() |
||||||
|
}; |
||||||
|
|
||||||
|
foreach ((string key, List<string> value) in GetMetadataGuids(metadata)) |
||||||
|
{ |
||||||
|
doc.AdditionalProperties.Add(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
await _client.IndexAsync(doc, IndexName); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
metadata.Artist = null; |
||||||
|
_logger.LogWarning(ex, "Error indexing artist with metadata {@Metadata}", metadata); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async Task UpdateMusicVideo(ISearchRepository searchRepository, MusicVideo musicVideo) |
||||||
|
{ |
||||||
|
foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata.HeadOrNone()) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
var doc = new ElasticSearchItem |
||||||
|
{ |
||||||
|
Id = musicVideo.Id, |
||||||
|
Type = LuceneSearchIndex.MusicVideoType, |
||||||
|
Title = metadata.Title, |
||||||
|
SortTitle = metadata.SortTitle.ToLowerInvariant(), |
||||||
|
LibraryName = musicVideo.LibraryPath.Library.Name, |
||||||
|
LibraryId = musicVideo.LibraryPath.Library.Id, |
||||||
|
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata), |
||||||
|
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), |
||||||
|
State = musicVideo.State.ToString(), |
||||||
|
MetadataKind = metadata.MetadataKind.ToString(), |
||||||
|
Language = await GetLanguages(searchRepository, musicVideo.MediaVersions), |
||||||
|
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), |
||||||
|
AddedDate = GetAddedDate(metadata.DateAdded), |
||||||
|
Album = metadata.Album ?? string.Empty, |
||||||
|
Plot = metadata.Plot ?? string.Empty, |
||||||
|
Genre = metadata.Genres.Map(g => g.Name).ToList(), |
||||||
|
Tag = metadata.Tags.Map(t => t.Name).ToList(), |
||||||
|
Studio = metadata.Studios.Map(s => s.Name).ToList() |
||||||
|
}; |
||||||
|
|
||||||
|
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); |
||||||
|
} |
||||||
|
|
||||||
|
doc.Artist = artists.ToList(); |
||||||
|
|
||||||
|
AddStatistics(doc, musicVideo.MediaVersions); |
||||||
|
|
||||||
|
foreach ((string key, List<string> value) in GetMetadataGuids(metadata)) |
||||||
|
{ |
||||||
|
doc.AdditionalProperties.Add(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
await _client.IndexAsync(doc, IndexName); |
||||||
|
} |
||||||
|
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.HeadOrNone()) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
var doc = new ElasticSearchItem |
||||||
|
{ |
||||||
|
Id = episode.Id, |
||||||
|
Type = LuceneSearchIndex.EpisodeType, |
||||||
|
Title = metadata.Title, |
||||||
|
SortTitle = metadata.SortTitle.ToLowerInvariant(), |
||||||
|
LibraryName = episode.LibraryPath.Library.Name, |
||||||
|
LibraryId = episode.LibraryPath.Library.Id, |
||||||
|
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata), |
||||||
|
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), |
||||||
|
State = episode.State.ToString(), |
||||||
|
MetadataKind = metadata.MetadataKind.ToString(), |
||||||
|
SeasonNumber = episode.Season?.SeasonNumber ?? 0, |
||||||
|
EpisodeNumber = metadata.EpisodeNumber, |
||||||
|
Language = await GetLanguages(searchRepository, episode.MediaVersions), |
||||||
|
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), |
||||||
|
AddedDate = GetAddedDate(metadata.DateAdded), |
||||||
|
Plot = metadata.Plot ?? string.Empty, |
||||||
|
Genre = metadata.Genres.Map(g => g.Name).ToList(), |
||||||
|
Tag = metadata.Tags.Map(t => t.Name).ToList(), |
||||||
|
Studio = metadata.Studios.Map(s => s.Name).ToList(), |
||||||
|
Actor = metadata.Actors.Map(a => a.Name).ToList(), |
||||||
|
Director = metadata.Directors.Map(d => d.Name).ToList(), |
||||||
|
Writer = metadata.Writers.Map(w => w.Name).ToList(), |
||||||
|
TraktList = episode.TraktListItems.Map(t => t.TraktList.TraktId.ToString()).ToList() |
||||||
|
}; |
||||||
|
|
||||||
|
// add some show fields to help filter episodes within a particular show
|
||||||
|
foreach (ShowMetadata showMetadata in Optional(episode.Season?.Show?.ShowMetadata).Flatten()) |
||||||
|
{ |
||||||
|
doc.ShowTitle = showMetadata.Title; |
||||||
|
doc.ShowGenre = showMetadata.Genres.Map(g => g.Name).ToList(); |
||||||
|
doc.ShowTag = showMetadata.Tags.Map(t => t.Name).ToList(); |
||||||
|
} |
||||||
|
|
||||||
|
AddStatistics(doc, episode.MediaVersions); |
||||||
|
|
||||||
|
foreach ((string key, List<string> value) in GetMetadataGuids(metadata)) |
||||||
|
{ |
||||||
|
doc.AdditionalProperties.Add(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
await _client.IndexAsync(doc, IndexName); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
metadata.Episode = null; |
||||||
|
_logger.LogWarning(ex, "Error indexing episode with metadata {@Metadata}", metadata); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async Task UpdateOtherVideo(ISearchRepository searchRepository, OtherVideo otherVideo) |
||||||
|
{ |
||||||
|
foreach (OtherVideoMetadata metadata in otherVideo.OtherVideoMetadata.HeadOrNone()) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
var doc = new ElasticSearchItem |
||||||
|
{ |
||||||
|
Id = otherVideo.Id, |
||||||
|
Type = LuceneSearchIndex.OtherVideoType, |
||||||
|
Title = metadata.Title, |
||||||
|
SortTitle = metadata.SortTitle.ToLowerInvariant(), |
||||||
|
LibraryName = otherVideo.LibraryPath.Library.Name, |
||||||
|
LibraryId = otherVideo.LibraryPath.Library.Id, |
||||||
|
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata), |
||||||
|
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), |
||||||
|
State = otherVideo.State.ToString(), |
||||||
|
MetadataKind = metadata.MetadataKind.ToString(), |
||||||
|
Language = await GetLanguages(searchRepository, otherVideo.MediaVersions), |
||||||
|
ContentRating = GetContentRatings(metadata.ContentRating), |
||||||
|
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), |
||||||
|
AddedDate = GetAddedDate(metadata.DateAdded), |
||||||
|
Plot = metadata.Plot ?? string.Empty, |
||||||
|
Genre = metadata.Genres.Map(g => g.Name).ToList(), |
||||||
|
Tag = metadata.Tags.Map(t => t.Name).ToList(), |
||||||
|
Studio = metadata.Studios.Map(s => s.Name).ToList(), |
||||||
|
Actor = metadata.Actors.Map(a => a.Name).ToList(), |
||||||
|
Director = metadata.Directors.Map(d => d.Name).ToList(), |
||||||
|
Writer = metadata.Writers.Map(w => w.Name).ToList() |
||||||
|
}; |
||||||
|
|
||||||
|
AddStatistics(doc, otherVideo.MediaVersions); |
||||||
|
|
||||||
|
foreach ((string key, List<string> value) in GetMetadataGuids(metadata)) |
||||||
|
{ |
||||||
|
doc.AdditionalProperties.Add(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
await _client.IndexAsync(doc, IndexName); |
||||||
|
} |
||||||
|
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) |
||||||
|
{ |
||||||
|
foreach (SongMetadata metadata in song.SongMetadata.HeadOrNone()) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
var doc = new ElasticSearchItem |
||||||
|
{ |
||||||
|
Id = song.Id, |
||||||
|
Type = LuceneSearchIndex.SongType, |
||||||
|
Title = metadata.Title, |
||||||
|
SortTitle = metadata.SortTitle.ToLowerInvariant(), |
||||||
|
LibraryName = song.LibraryPath.Library.Name, |
||||||
|
LibraryId = song.LibraryPath.Library.Id, |
||||||
|
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata), |
||||||
|
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata), |
||||||
|
State = song.State.ToString(), |
||||||
|
MetadataKind = metadata.MetadataKind.ToString(), |
||||||
|
Language = await GetLanguages(searchRepository, song.MediaVersions), |
||||||
|
AddedDate = GetAddedDate(metadata.DateAdded), |
||||||
|
Album = metadata.Album ?? string.Empty, |
||||||
|
Artist = !string.IsNullOrWhiteSpace(metadata.Artist) ? new List<string> { metadata.Artist } : null, |
||||||
|
AlbumArtist = metadata.AlbumArtist, |
||||||
|
Genre = metadata.Genres.Map(g => g.Name).ToList(), |
||||||
|
Tag = metadata.Tags.Map(t => t.Name).ToList() |
||||||
|
}; |
||||||
|
|
||||||
|
AddStatistics(doc, song.MediaVersions); |
||||||
|
|
||||||
|
foreach ((string key, List<string> value) in GetMetadataGuids(metadata)) |
||||||
|
{ |
||||||
|
doc.AdditionalProperties.Add(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
await _client.IndexAsync(doc, IndexName); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
metadata.Song = null; |
||||||
|
_logger.LogWarning(ex, "Error indexing song with metadata {@Metadata}", metadata); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static string GetReleaseDate(DateTime? metadataReleaseDate) => metadataReleaseDate?.ToString("yyyyMMdd"); |
||||||
|
|
||||||
|
private static string GetAddedDate(DateTime metadataAddedDate) => metadataAddedDate.ToString("yyyyMMdd"); |
||||||
|
|
||||||
|
private static List<string> GetContentRatings(string metadataContentRating) |
||||||
|
{ |
||||||
|
var contentRatings = new List<string>(); |
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(metadataContentRating)) |
||||||
|
{ |
||||||
|
foreach (string contentRating in metadataContentRating.Split("/") |
||||||
|
.Map(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x))) |
||||||
|
{ |
||||||
|
contentRatings.Add(contentRating); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return contentRatings; |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<List<string>> GetLanguages( |
||||||
|
ISearchRepository searchRepository, |
||||||
|
IEnumerable<MediaVersion> mediaVersions) |
||||||
|
{ |
||||||
|
var result = new List<string>(); |
||||||
|
|
||||||
|
foreach (MediaVersion version in mediaVersions.HeadOrNone()) |
||||||
|
{ |
||||||
|
var mediaCodes = version.Streams |
||||||
|
.Filter(ms => ms.MediaStreamKind == MediaStreamKind.Audio) |
||||||
|
.Map(ms => ms.Language) |
||||||
|
.Distinct() |
||||||
|
.ToList(); |
||||||
|
|
||||||
|
result.AddRange(await GetLanguages(searchRepository, mediaCodes)); |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<List<string>> GetLanguages(ISearchRepository searchRepository, List<string> mediaCodes) |
||||||
|
{ |
||||||
|
var englishNames = new System.Collections.Generic.HashSet<string>(); |
||||||
|
foreach (string code in await searchRepository.GetAllLanguageCodes(mediaCodes)) |
||||||
|
{ |
||||||
|
Option<CultureInfo> maybeCultureInfo = _cultureInfos.Find( |
||||||
|
ci => string.Equals(ci.ThreeLetterISOLanguageName, code, StringComparison.OrdinalIgnoreCase)); |
||||||
|
foreach (CultureInfo cultureInfo in maybeCultureInfo) |
||||||
|
{ |
||||||
|
englishNames.Add(cultureInfo.EnglishName); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return englishNames.ToList(); |
||||||
|
} |
||||||
|
|
||||||
|
private static void AddStatistics(ElasticSearchItem doc, IEnumerable<MediaVersion> mediaVersions) |
||||||
|
{ |
||||||
|
foreach (MediaVersion version in mediaVersions.HeadOrNone()) |
||||||
|
{ |
||||||
|
doc.Minutes = (int)Math.Ceiling(version.Duration.TotalMinutes); |
||||||
|
|
||||||
|
foreach (MediaStream videoStream in version.Streams |
||||||
|
.Filter(s => s.MediaStreamKind == MediaStreamKind.Video) |
||||||
|
.HeadOrNone()) |
||||||
|
{ |
||||||
|
doc.Height = version.Height; |
||||||
|
doc.Width = version.Width; |
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(videoStream.Codec)) |
||||||
|
{ |
||||||
|
doc.VideoCodec = videoStream.Codec; |
||||||
|
} |
||||||
|
|
||||||
|
Option<IPixelFormat> maybePixelFormat = |
||||||
|
AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, null); |
||||||
|
foreach (IPixelFormat pixelFormat in maybePixelFormat) |
||||||
|
{ |
||||||
|
doc.VideoBitDepth = pixelFormat.BitDepth; |
||||||
|
} |
||||||
|
|
||||||
|
var colorParams = new ColorParams( |
||||||
|
videoStream.ColorRange, |
||||||
|
videoStream.ColorSpace, |
||||||
|
videoStream.ColorTransfer, |
||||||
|
videoStream.ColorPrimaries); |
||||||
|
|
||||||
|
doc.VideoDynamicRange = colorParams.IsHdr ? "hdr" : "sdr"; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static Dictionary<string, List<string>> GetMetadataGuids(Metadata metadata) |
||||||
|
{ |
||||||
|
var result = new Dictionary<string, List<string>>(); |
||||||
|
|
||||||
|
foreach (MetadataGuid guid in metadata.Guids) |
||||||
|
{ |
||||||
|
string[] split = (guid.Guid ?? string.Empty).Split("://"); |
||||||
|
if (split.Length == 2 && !string.IsNullOrWhiteSpace(split[1])) |
||||||
|
{ |
||||||
|
string key = split[0]; |
||||||
|
string value = split[1].ToLowerInvariant(); |
||||||
|
if (!result.ContainsKey(key)) |
||||||
|
{ |
||||||
|
result.Add(key, new List<string>()); |
||||||
|
} |
||||||
|
|
||||||
|
result[key].Add(value); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Option<SearchPageMap>> GetSearchPageMap(string query, int limit) |
||||||
|
{ |
||||||
|
SearchResponse<MinimalElasticSearchItem> response = await _client.SearchAsync<MinimalElasticSearchItem>( |
||||||
|
s => s.Index(IndexName) |
||||||
|
.Size(0) |
||||||
|
.Sort(ss => ss.Field(f => f.SortTitle, fs => fs.Order(SortOrder.Asc))) |
||||||
|
.Aggregations(a => a.Terms("count", v => v.Field(i => i.JumpLetter).Size(30))) |
||||||
|
.QueryLuceneSyntax(query)); |
||||||
|
|
||||||
|
if (!response.IsValidResponse) |
||||||
|
{ |
||||||
|
return Option<SearchPageMap>.None; |
||||||
|
} |
||||||
|
|
||||||
|
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); |
||||||
|
|
||||||
|
if (response.Aggregations?.Values.Head() is StringTermsAggregate aggregate) |
||||||
|
{ |
||||||
|
// start on page 1
|
||||||
|
int total = limit; |
||||||
|
|
||||||
|
foreach (char letter in letters) |
||||||
|
{ |
||||||
|
map[letter] = total / limit; |
||||||
|
|
||||||
|
Option<StringTermsBucket> maybeBucket = aggregate.Buckets.Find(b => b.Key == letter.ToString()); |
||||||
|
total += maybeBucket.Sum(bucket => (int)bucket.DocCount); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return new SearchPageMap(map); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,111 @@ |
|||||||
|
using System.Text.Json.Serialization; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Search.Models; |
||||||
|
|
||||||
|
public class ElasticSearchItem : MinimalElasticSearchItem |
||||||
|
{ |
||||||
|
[JsonExtensionData] |
||||||
|
public Dictionary<string, object> AdditionalProperties { get; set; } = new(); |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.TitleField)] |
||||||
|
public string Title { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.LibraryNameField)] |
||||||
|
public string LibraryName { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.LibraryIdField)] |
||||||
|
public int LibraryId { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.TitleAndYearField)] |
||||||
|
public string TitleAndYear { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.StateField)] |
||||||
|
public string State { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.MetadataKindField)] |
||||||
|
public string MetadataKind { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.LanguageField)] |
||||||
|
public List<string> Language { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.MinutesField)] |
||||||
|
public int Minutes { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.HeightField)] |
||||||
|
public int Height { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.WidthField)] |
||||||
|
public int Width { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.VideoCodecField)] |
||||||
|
public string VideoCodec { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.VideoBitDepthField)] |
||||||
|
public int VideoBitDepth { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.VideoDynamicRange)] |
||||||
|
public string VideoDynamicRange { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.ContentRatingField)] |
||||||
|
public List<string> ContentRating { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.ReleaseDateField)] |
||||||
|
public string ReleaseDate { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.AddedDateField)] |
||||||
|
public string AddedDate { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.AlbumField)] |
||||||
|
public string Album { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.AlbumArtistField)] |
||||||
|
public string AlbumArtist { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.PlotField)] |
||||||
|
public string Plot { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.GenreField)] |
||||||
|
public List<string> Genre { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.TagField)] |
||||||
|
public List<string> Tag { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.StudioField)] |
||||||
|
public List<string> Studio { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.ArtistField)] |
||||||
|
public List<string> Artist { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.ActorField)] |
||||||
|
public List<string> Actor { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.DirectorField)] |
||||||
|
public List<string> Director { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.WriterField)] |
||||||
|
public List<string> Writer { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.TraktListField)] |
||||||
|
public List<string> TraktList { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.SeasonNumberField)] |
||||||
|
public int SeasonNumber { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.EpisodeNumberField)] |
||||||
|
public int EpisodeNumber { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.ShowTitleField)] |
||||||
|
public string ShowTitle { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.ShowGenreField)] |
||||||
|
public List<string> ShowGenre { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.ShowTagField)] |
||||||
|
public List<string> ShowTag { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.StyleField)] |
||||||
|
public List<string> Style { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.MoodField)] |
||||||
|
public List<string> Mood { get; set; } |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
using System.Text.Json.Serialization; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Search.Models; |
||||||
|
|
||||||
|
public class MinimalElasticSearchItem |
||||||
|
{ |
||||||
|
[JsonPropertyName(LuceneSearchIndex.IdField)] |
||||||
|
public int Id { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.TypeField)] |
||||||
|
public string Type { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.SortTitleField)] |
||||||
|
public string SortTitle { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName(LuceneSearchIndex.JumpLetterField)] |
||||||
|
public string JumpLetter { get; set; } |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
namespace ErsatzTV; |
||||||
|
|
||||||
|
public static class SearchHelper |
||||||
|
{ |
||||||
|
public static string ElasticSearchUri { get; private set; } |
||||||
|
public static string ElasticSearchIndexName { get; private set; } |
||||||
|
public static bool IsElasticSearchEnabled { get; private set; } |
||||||
|
|
||||||
|
public static void Init(IConfiguration configuration) |
||||||
|
{ |
||||||
|
ElasticSearchUri = configuration["ElasticSearch:Uri"]; |
||||||
|
ElasticSearchIndexName = configuration["ElasticSearch:IndexName"]; |
||||||
|
if (string.IsNullOrWhiteSpace(ElasticSearchIndexName)) |
||||||
|
{ |
||||||
|
ElasticSearchIndexName = "ersatztv"; |
||||||
|
} |
||||||
|
|
||||||
|
IsElasticSearchEnabled = !string.IsNullOrWhiteSpace(ElasticSearchUri); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue