diff --git a/CHANGELOG.md b/CHANGELOG.md index 076b4fe93..2d8590c6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - `isForced`: bool indicating whether the stream is flagged as forced - `language`: the stream's language - `title`: the stream's title +- Add new fields to search index + - `video_codec`: the video codec + - `video_bit_depth`: the number of bits in the video stream's pixel format, e.g. 8 or 10 + - `video_dynamic_range`: the video's dynamic range, either `sdr` or `hdr` ### Changed - Change `Multi-Episode Shuffle` scripting system to use Javascript instead of Lua diff --git a/ErsatzTV.FFmpeg/Format/AvailablePixelFormats.cs b/ErsatzTV.FFmpeg/Format/AvailablePixelFormats.cs index bf58a60a1..027a6b322 100644 --- a/ErsatzTV.FFmpeg/Format/AvailablePixelFormats.cs +++ b/ErsatzTV.FFmpeg/Format/AvailablePixelFormats.cs @@ -4,7 +4,7 @@ namespace ErsatzTV.FFmpeg.Format; public static class AvailablePixelFormats { - public static Option ForPixelFormat(string pixelFormat, ILogger logger) => + public static Option ForPixelFormat(string pixelFormat, ILogger? logger) => pixelFormat switch { PixelFormat.YUV420P => new PixelFormatYuv420P(), @@ -15,9 +15,9 @@ public static class AvailablePixelFormats _ => LogUnknownPixelFormat(pixelFormat, logger) }; - private static Option LogUnknownPixelFormat(string pixelFormat, ILogger logger) + private static Option LogUnknownPixelFormat(string pixelFormat, ILogger? logger) { - logger.LogWarning("Unexpected pixel format {PixelFormat} may have playback issues", pixelFormat); + logger?.LogWarning("Unexpected pixel format {PixelFormat} may have playback issues", pixelFormat); return Option.None; } } diff --git a/ErsatzTV.Infrastructure/Search/CustomQueryParser.cs b/ErsatzTV.Infrastructure/Search/CustomQueryParser.cs index d86b4dee7..eb60918c9 100644 --- a/ErsatzTV.Infrastructure/Search/CustomQueryParser.cs +++ b/ErsatzTV.Infrastructure/Search/CustomQueryParser.cs @@ -16,7 +16,8 @@ public class CustomQueryParser : QueryParser SearchIndex.HeightField, SearchIndex.WidthField, SearchIndex.SeasonNumberField, - SearchIndex.EpisodeNumberField + SearchIndex.EpisodeNumberField, + SearchIndex.VideoBitDepthField }; public CustomQueryParser(LuceneVersion matchVersion, string f, Analyzer a) : base(matchVersion, f, a) diff --git a/ErsatzTV.Infrastructure/Search/SearchIndex.cs b/ErsatzTV.Infrastructure/Search/SearchIndex.cs index 0883105cf..5d98577a4 100644 --- a/ErsatzTV.Infrastructure/Search/SearchIndex.cs +++ b/ErsatzTV.Infrastructure/Search/SearchIndex.cs @@ -6,6 +6,8 @@ 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.Analysis.Core; @@ -20,6 +22,7 @@ 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; @@ -56,6 +59,8 @@ public sealed class SearchIndex : ISearchIndex private const string ShowGenreField = "show_genre"; private const string ShowTagField = "show_tag"; private const string MetadataKindField = "metadata_kind"; + private const string VideoCodecField = "video_codec"; + private const string VideoDynamicRange = "video_dynamic_range"; internal const string MinutesField = "minutes"; internal const string HeightField = "height"; @@ -64,6 +69,7 @@ public sealed class SearchIndex : ISearchIndex 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"; @@ -73,7 +79,7 @@ public sealed class SearchIndex : ISearchIndex public const string EpisodeType = "episode"; public const string OtherVideoType = "other_video"; public const string SongType = "song"; - + private readonly List _cultureInfos; private readonly ILogger _logger; @@ -89,7 +95,7 @@ public sealed class SearchIndex : ISearchIndex _initialized = false; } - public int Version => 33; + public int Version => 34; public async Task Initialize( ILocalFileSystem localFileSystem, @@ -382,14 +388,7 @@ public sealed class SearchIndex : ISearchIndex await AddLanguages(searchRepository, doc, movie.MediaVersions); - foreach (MediaVersion version in movie.MediaVersions.HeadOrNone()) - { - doc.Add( - 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)); - } + AddStatistics(doc, movie.MediaVersions); if (!string.IsNullOrWhiteSpace(metadata.ContentRating)) { @@ -752,14 +751,7 @@ public sealed class SearchIndex : ISearchIndex await AddLanguages(searchRepository, doc, musicVideo.MediaVersions); - foreach (MediaVersion version in musicVideo.MediaVersions.HeadOrNone()) - { - doc.Add( - 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)); - } + AddStatistics(doc, musicVideo.MediaVersions); if (metadata.ReleaseDate.HasValue) { @@ -891,14 +883,7 @@ public sealed class SearchIndex : ISearchIndex await AddLanguages(searchRepository, doc, episode.MediaVersions); - foreach (MediaVersion version in episode.MediaVersions.HeadOrNone()) - { - doc.Add( - 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)); - } + AddStatistics(doc, episode.MediaVersions); if (metadata.ReleaseDate.HasValue) { @@ -988,14 +973,7 @@ public sealed class SearchIndex : ISearchIndex await AddLanguages(searchRepository, doc, otherVideo.MediaVersions); - foreach (MediaVersion version in otherVideo.MediaVersions.HeadOrNone()) - { - doc.Add( - 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)); - } + AddStatistics(doc, otherVideo.MediaVersions); if (!string.IsNullOrWhiteSpace(metadata.ContentRating)) { @@ -1089,11 +1067,7 @@ public sealed class SearchIndex : ISearchIndex await AddLanguages(searchRepository, doc, song.MediaVersions); - foreach (MediaVersion version in song.MediaVersions.HeadOrNone()) - { - doc.Add( - new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO)); - } + AddStatistics(doc, song.MediaVersions); doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO)); @@ -1152,6 +1126,42 @@ public sealed class SearchIndex : ISearchIndex return query; } + + private void AddStatistics(Document doc, List mediaVersions) + { + foreach (MediaVersion version in mediaVersions) + { + doc.Add(new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), 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)) + { + doc.Add(new StringField(VideoCodecField, videoStream.Codec, Field.Store.NO)); + + Option maybePixelFormat = + AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, null); + foreach (IPixelFormat pixelFormat in maybePixelFormat) + { + doc.Add(new Int32Field(VideoBitDepthField, pixelFormat.BitDepth, Field.Store.NO)); + } + + var colorParams = new ColorParams( + videoStream.ColorRange, + videoStream.ColorSpace, + videoStream.ColorTransfer, + videoStream.ColorPrimaries); + + string dynamicRange = colorParams.IsHdr ? "hdr" : "sdr"; + + doc.Add(new TextField(VideoDynamicRange, dynamicRange, Field.Store.NO)); + } + } + } private static void AddMetadataGuids(Metadata metadata, Document doc) { diff --git a/docs/user-guide/search.md b/docs/user-guide/search.md index 36892d819..314b84220 100644 --- a/docs/user-guide/search.md +++ b/docs/user-guide/search.md @@ -28,6 +28,9 @@ The following fields are available for searching movies: - `minutes`: The rounded-up whole number duration of the movie in minutes - `height`: The movie height - `width`: The movie width +- `video_codec`: The video codec +- `video_bit_depth`: The number of bits in the movie's pixel format +- `video_dynamic_range`: The movie's dynamic range (`sdr` or `hdr`) - `type`: Always `movie` ### Shows @@ -79,6 +82,9 @@ The following fields are available for searching episodes: - `show_title`: The title of the show that contains the episode - `show_genre`: The genre of the show that contains the episode - `show_tag`: The tag of the show that contains the episode +- `video_codec`: The video codec +- `video_bit_depth`: The number of bits in the episode's pixel format +- `video_dynamic_range`: The episode's dynamic range (`sdr` or `hdr`) - `type`: Always `episode` ### Artists @@ -108,6 +114,9 @@ The following fields are available for searching music videos: - `minutes`: The rounded-up whole number duration of the music video in minutes - `height`: The music video height - `width`: The music video width +- `video_codec`: The video codec +- `video_bit_depth`: The number of bits in the music video's pixel format +- `video_dynamic_range`: The music video's dynamic range (`sdr` or `hdr`) - `type`: Always `music_video` ### Other Videos @@ -130,6 +139,9 @@ The following fields are available for searching other videos: - `minutes`: The rounded-up whole number duration of the video in minutes - `height`: The video height - `width`: The video width +- `video_codec`: The video codec +- `video_bit_depth`: The number of bits in the other video's pixel format +- `video_dynamic_range`: The other video's dynamic range (`sdr` or `hdr`) - `type`: Always `other_video` ### Songs