Browse Source

add more search fields (#766)

* properly index show and season state

* add height, width, season_number, episode_number to search index

* update docs
pull/769/head
Jason Dove 4 years ago committed by GitHub
parent
commit
837f311ec0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 18
      ErsatzTV.Infrastructure/Health/Checks/FileNotFoundHealthCheck.cs
  3. 25
      ErsatzTV.Infrastructure/Health/Checks/UnavailableHealthCheck.cs
  4. 4
      ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs
  5. 9
      ErsatzTV.Infrastructure/Search/CustomMultiFieldQueryParser.cs
  6. 15
      ErsatzTV.Infrastructure/Search/CustomQueryParser.cs
  7. 30
      ErsatzTV.Infrastructure/Search/SearchIndex.cs
  8. 16
      docs/user-guide/search.md

2
CHANGELOG.md

@ -16,6 +16,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added ### Added
- Add `unavailable` state for Emby movie libraries - Add `unavailable` state for Emby movie libraries
- Add `height` and `width` to search index for all videos
- Add `season_number` and `episode_number` to search index for all episodes
## [0.5.3-beta] - 2022-04-24 ## [0.5.3-beta] - 2022-04-24
### Fixed ### Fixed

18
ErsatzTV.Infrastructure/Health/Checks/FileNotFoundHealthCheck.cs

@ -31,20 +31,26 @@ public class FileNotFoundHealthCheck : BaseHealthCheck, IFileNotFoundHealthCheck
.Include(mi => (mi as OtherVideo).MediaVersions) .Include(mi => (mi as OtherVideo).MediaVersions)
.ThenInclude(mv => mv.MediaFiles) .ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as Song).MediaVersions) .Include(mi => (mi as Song).MediaVersions)
.ThenInclude(mv => mv.MediaFiles); .ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as Show).ShowMetadata)
.Include(mi => (mi as Season).Show)
.ThenInclude(s => s.ShowMetadata)
.Include(mi => (mi as Season).SeasonMetadata);
List<MediaItem> five = await mediaItems List<MediaItem> five = await mediaItems
// shows and seasons don't have paths to display
.Filter(mi => !(mi is Show))
.Filter(mi => !(mi is Season))
.OrderBy(mi => mi.Id) .OrderBy(mi => mi.Id)
.Take(5) .Take(5)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (mediaItems.Any()) if (mediaItems.Any())
{ {
IEnumerable<string> paths = five.Map(mi => mi.GetHeadVersion().MediaFiles.Head().Path); IEnumerable<string> paths = five.Map(
mi => mi switch
{
Show s => s.ShowMetadata.Head().Title,
Season s => $"{s.Show.ShowMetadata.Head().Title} Season {s.SeasonNumber}",
_ => mi.GetHeadVersion().MediaFiles.Head().Path
});
var files = string.Join(", ", paths); var files = string.Join(", ", paths);

25
ErsatzTV.Infrastructure/Health/Checks/UnavailableHealthCheck.cs

@ -46,13 +46,13 @@ public class UnavailableHealthCheck : BaseHealthCheck, IUnavailableHealthCheck
.Include(mi => (mi as OtherVideo).MediaVersions) .Include(mi => (mi as OtherVideo).MediaVersions)
.ThenInclude(mv => mv.MediaFiles) .ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as Song).MediaVersions) .Include(mi => (mi as Song).MediaVersions)
.ThenInclude(mv => mv.MediaFiles); .ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as Show).ShowMetadata)
.Include(mi => (mi as Season).Show)
.ThenInclude(s => s.ShowMetadata)
.Include(mi => (mi as Season).SeasonMetadata);
List<MediaItem> five = await mediaItems List<MediaItem> five = await mediaItems
// shows and seasons don't have paths to display
.Filter(mi => !(mi is Show))
.Filter(mi => !(mi is Season))
.OrderBy(mi => mi.Id) .OrderBy(mi => mi.Id)
.Take(5) .Take(5)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@ -63,11 +63,16 @@ public class UnavailableHealthCheck : BaseHealthCheck, IUnavailableHealthCheck
foreach (MediaItem mediaItem in five) foreach (MediaItem mediaItem in five)
{ {
string path = await mediaItem.GetLocalPath( string path = mediaItem switch
_plexPathReplacementService, {
_jellyfinPathReplacementService, Show s => s.ShowMetadata.Head().Title,
_embyPathReplacementService, Season s => $"{s.Show.ShowMetadata.Head().Title} Season {s.SeasonNumber}",
false); _ => await mediaItem.GetLocalPath(
_plexPathReplacementService,
_jellyfinPathReplacementService,
_embyPathReplacementService,
false)
};
paths.Add(path); paths.Add(path);
} }

4
ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

@ -100,7 +100,9 @@ public class PlexServerApiClient : IPlexServerApiClient
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri); IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
return await service.GetLibrarySectionContents(library.Key, token.AuthToken) return await service.GetLibrarySectionContents(library.Key, token.AuthToken)
.Map(r => r.MediaContainer.Metadata) .Map(r => r.MediaContainer.Metadata)
.Map(list => list.Map(metadata => ProjectToShow(metadata, library.MediaSourceId)).ToList()); .Map(
list => (list ?? new List<PlexMetadataResponse>())
.Map(metadata => ProjectToShow(metadata, library.MediaSourceId)).ToList());
} }
catch (Exception ex) catch (Exception ex)
{ {

9
ErsatzTV.Infrastructure/Search/CustomMultiFieldQueryParser.cs

@ -32,11 +32,11 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
return base.GetWildcardQuery("release_date", todayString); return base.GetWildcardQuery("release_date", todayString);
} }
if (field == "minutes" && int.TryParse(queryText, out int val)) if (CustomQueryParser.NumericFields.Contains(field) && int.TryParse(queryText, out int val))
{ {
var bytesRef = new BytesRef(); var bytesRef = new BytesRef();
NumericUtils.Int32ToPrefixCoded(val, 0, bytesRef); NumericUtils.Int32ToPrefixCoded(val, 0, bytesRef);
return NewTermQuery(new Term("minutes", bytesRef)); return NewTermQuery(new Term(field, bytesRef));
} }
return base.GetFieldQuery(field, queryText, quoted); return base.GetFieldQuery(field, queryText, quoted);
@ -84,9 +84,10 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
bool startInclusive, bool startInclusive,
bool endInclusive) bool endInclusive)
{ {
if (field == "minutes" && int.TryParse(part1, out int min) && int.TryParse(part2, out int max)) if (CustomQueryParser.NumericFields.Contains(field) && int.TryParse(part1, out int min) &&
int.TryParse(part2, out int max))
{ {
return NumericRangeQuery.NewInt32Range(field, min, max, startInclusive, endInclusive); return NumericRangeQuery.NewInt32Range(field, 1, min, max, startInclusive, endInclusive);
} }
return base.GetRangeQuery(field, part1, part2, startInclusive, endInclusive); return base.GetRangeQuery(field, part1, part2, startInclusive, endInclusive);

15
ErsatzTV.Infrastructure/Search/CustomQueryParser.cs

@ -10,6 +10,15 @@ namespace ErsatzTV.Infrastructure.Search;
public class CustomQueryParser : QueryParser public class CustomQueryParser : QueryParser
{ {
internal static readonly List<string> NumericFields = new()
{
SearchIndex.MinutesField,
SearchIndex.HeightField,
SearchIndex.WidthField,
SearchIndex.SeasonNumberField,
SearchIndex.EpisodeNumberField
};
public CustomQueryParser(LuceneVersion matchVersion, string f, Analyzer a) : base(matchVersion, f, a) public CustomQueryParser(LuceneVersion matchVersion, string f, Analyzer a) : base(matchVersion, f, a)
{ {
} }
@ -30,11 +39,11 @@ public class CustomQueryParser : QueryParser
return base.GetWildcardQuery("release_date", todayString); return base.GetWildcardQuery("release_date", todayString);
} }
if (field == "minutes" && int.TryParse(queryText, out int val)) if (NumericFields.Contains(field) && int.TryParse(queryText, out int val))
{ {
var bytesRef = new BytesRef(); var bytesRef = new BytesRef();
NumericUtils.Int32ToPrefixCoded(val, 0, bytesRef); NumericUtils.Int32ToPrefixCoded(val, 0, bytesRef);
return NewTermQuery(new Term("minutes", bytesRef)); return NewTermQuery(new Term(field, bytesRef));
} }
return base.GetFieldQuery(field, queryText, quoted); return base.GetFieldQuery(field, queryText, quoted);
@ -82,7 +91,7 @@ public class CustomQueryParser : QueryParser
bool startInclusive, bool startInclusive,
bool endInclusive) bool endInclusive)
{ {
if (field == "minutes" && int.TryParse(part1, out int min) && int.TryParse(part2, out int max)) if (NumericFields.Contains(field) && int.TryParse(part1, out int min) && int.TryParse(part2, out int max))
{ {
return NumericRangeQuery.NewInt32Range(field, 1, min, max, startInclusive, endInclusive); return NumericRangeQuery.NewInt32Range(field, 1, min, max, startInclusive, endInclusive);
} }

30
ErsatzTV.Infrastructure/Search/SearchIndex.cs

@ -49,10 +49,14 @@ public sealed class SearchIndex : ISearchIndex
private const string WriterField = "writer"; private const string WriterField = "writer";
private const string TraktListField = "trakt_list"; private const string TraktListField = "trakt_list";
private const string AlbumField = "album"; private const string AlbumField = "album";
private const string MinutesField = "minutes"; internal const string MinutesField = "minutes";
private const string ArtistField = "artist"; private const string ArtistField = "artist";
private const string StateField = "state"; private const string StateField = "state";
private const string AlbumArtistField = "album_artist"; private const string AlbumArtistField = "album_artist";
internal const string HeightField = "height";
internal const string WidthField = "width";
internal const string SeasonNumberField = "season_number";
internal const string EpisodeNumberField = "episode_number";
public const string MovieType = "movie"; public const string MovieType = "movie";
public const string ShowType = "show"; public const string ShowType = "show";
@ -77,7 +81,7 @@ public sealed class SearchIndex : ISearchIndex
_initialized = false; _initialized = false;
} }
public int Version => 20; public int Version => 21;
public Task<bool> Initialize(ILocalFileSystem localFileSystem) public Task<bool> Initialize(ILocalFileSystem localFileSystem)
{ {
@ -329,6 +333,9 @@ public sealed class SearchIndex : ISearchIndex
{ {
doc.Add( doc.Add(
new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO)); new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO));
doc.Add(new Int32Field(HeightField, version.Height, Field.Store.NO));
doc.Add(new Int32Field(WidthField, version.Width, Field.Store.NO));
} }
if (!string.IsNullOrWhiteSpace(metadata.ContentRating)) if (!string.IsNullOrWhiteSpace(metadata.ContentRating))
@ -453,7 +460,8 @@ public sealed class SearchIndex : ISearchIndex
new TextField(LibraryNameField, show.LibraryPath.Library.Name, Field.Store.NO), new TextField(LibraryNameField, show.LibraryPath.Library.Name, Field.Store.NO),
new StringField(LibraryIdField, show.LibraryPath.Library.Id.ToString(), Field.Store.NO), new StringField(LibraryIdField, show.LibraryPath.Library.Id.ToString(), Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO), new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES) new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES),
new StringField(StateField, show.State.ToString(), Field.Store.NO)
}; };
List<string> languages = await searchRepository.GetLanguagesForShow(show); List<string> languages = await searchRepository.GetLanguagesForShow(show);
@ -545,7 +553,8 @@ public sealed class SearchIndex : ISearchIndex
new TextField(LibraryNameField, season.LibraryPath.Library.Name, Field.Store.NO), new TextField(LibraryNameField, season.LibraryPath.Library.Name, Field.Store.NO),
new StringField(LibraryIdField, season.LibraryPath.Library.Id.ToString(), Field.Store.NO), new StringField(LibraryIdField, season.LibraryPath.Library.Id.ToString(), Field.Store.NO),
new StringField(TitleAndYearField, titleAndYear, Field.Store.NO), new StringField(TitleAndYearField, titleAndYear, Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(showMetadata), Field.Store.YES) new StringField(JumpLetterField, GetJumpLetter(showMetadata), Field.Store.YES),
new StringField(StateField, season.State.ToString(), Field.Store.NO)
}; };
List<string> languages = await searchRepository.GetLanguagesForSeason(season); List<string> languages = await searchRepository.GetLanguagesForSeason(season);
@ -670,6 +679,9 @@ public sealed class SearchIndex : ISearchIndex
{ {
doc.Add( doc.Add(
new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO)); new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO));
doc.Add(new Int32Field(HeightField, version.Height, Field.Store.NO));
doc.Add(new Int32Field(WidthField, version.Width, Field.Store.NO));
} }
if (metadata.ReleaseDate.HasValue) if (metadata.ReleaseDate.HasValue)
@ -753,7 +765,9 @@ public sealed class SearchIndex : ISearchIndex
new StringField(LibraryIdField, episode.LibraryPath.Library.Id.ToString(), Field.Store.NO), new StringField(LibraryIdField, episode.LibraryPath.Library.Id.ToString(), Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO), new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES), new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES),
new StringField(StateField, episode.State.ToString(), Field.Store.NO) 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)
}; };
await AddLanguages(searchRepository, doc, episode.MediaVersions); await AddLanguages(searchRepository, doc, episode.MediaVersions);
@ -762,6 +776,9 @@ public sealed class SearchIndex : ISearchIndex
{ {
doc.Add( doc.Add(
new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO)); new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO));
doc.Add(new Int32Field(HeightField, version.Height, Field.Store.NO));
doc.Add(new Int32Field(WidthField, version.Width, Field.Store.NO));
} }
if (metadata.ReleaseDate.HasValue) if (metadata.ReleaseDate.HasValue)
@ -853,6 +870,9 @@ public sealed class SearchIndex : ISearchIndex
{ {
doc.Add( doc.Add(
new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO)); new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO));
doc.Add(new Int32Field(HeightField, version.Height, Field.Store.NO));
doc.Add(new Int32Field(WidthField, version.Width, Field.Store.NO));
} }
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO)); doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));

16
docs/user-guide/search.md

@ -25,7 +25,9 @@ The following fields are available for searching movies:
- `language`: The movie audio stream language - `language`: The movie audio stream language
- `release_date`: The movie release date (YYYYMMDD) - `release_date`: The movie release date (YYYYMMDD)
- `added_date`: The date the movie was added to ErsatzTV (YYYYMMDD) - `added_date`: The date the movie was added to ErsatzTV (YYYYMMDD)
- `minutes`: the rounded-up whole number duration of the movie in minutes - `minutes`: The rounded-up whole number duration of the movie in minutes
- `height`: The movie height
- `width`: The movie width
- `type`: Always `movie` - `type`: Always `movie`
### Shows ### Shows
@ -57,7 +59,11 @@ The following fields are available for searching episodes:
- `language`: The episode audio stream language - `language`: The episode audio stream language
- `release_date`: The episode release date (YYYYMMDD) - `release_date`: The episode release date (YYYYMMDD)
- `added_date`: The date the episode was added to ErsatzTV (YYYYMMDD) - `added_date`: The date the episode was added to ErsatzTV (YYYYMMDD)
- `minutes`: the rounded-up whole number duration of the episode in minutes - `minutes`: The rounded-up whole number duration of the episode in minutes
- `height`: The episode height
- `width`: The episode width
- `season_number`: The episode season number
- `episode_number`: The episode number
- `type`: Always `episode` - `type`: Always `episode`
### Artists ### Artists
@ -84,7 +90,9 @@ The following fields are available for searching music videos:
- `language`: The music video audio stream language - `language`: The music video audio stream language
- `release_date`: The music video release date (YYYYMMDD) - `release_date`: The music video release date (YYYYMMDD)
- `added_date`: The date the music video was added to ErsatzTV (YYYYMMDD) - `added_date`: The date the music video was added to ErsatzTV (YYYYMMDD)
- `minutes`: the rounded-up whole number duration of the music video in minutes - `minutes`: The rounded-up whole number duration of the music video in minutes
- `height`: The music video height
- `width`: The music video width
- `type`: Always `music_video` - `type`: Always `music_video`
### Other Videos ### Other Videos
@ -95,6 +103,8 @@ The following fields are available for searching other videos:
- `tag`: All of the video's parent folders - `tag`: All of the video's parent folders
- `minutes`: the rounded-up whole number duration of the video in minutes - `minutes`: the rounded-up whole number duration of the video in minutes
- `added_date`: The date the other video was added to ErsatzTV (YYYYMMDD) - `added_date`: The date the other video was added to ErsatzTV (YYYYMMDD)
- `height`: The other video height
- `width`: The other video width
- `type`: Always `other_video` - `type`: Always `other_video`
### Songs ### Songs

Loading…
Cancel
Save