add video stats to search index (#1033)
This commit is contained in:
@@ -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
|
- `isForced`: bool indicating whether the stream is flagged as forced
|
||||||
- `language`: the stream's language
|
- `language`: the stream's language
|
||||||
- `title`: the stream's title
|
- `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
|
### Changed
|
||||||
- Change `Multi-Episode Shuffle` scripting system to use Javascript instead of Lua
|
- Change `Multi-Episode Shuffle` scripting system to use Javascript instead of Lua
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace ErsatzTV.FFmpeg.Format;
|
|||||||
|
|
||||||
public static class AvailablePixelFormats
|
public static class AvailablePixelFormats
|
||||||
{
|
{
|
||||||
public static Option<IPixelFormat> ForPixelFormat(string pixelFormat, ILogger logger) =>
|
public static Option<IPixelFormat> ForPixelFormat(string pixelFormat, ILogger? logger) =>
|
||||||
pixelFormat switch
|
pixelFormat switch
|
||||||
{
|
{
|
||||||
PixelFormat.YUV420P => new PixelFormatYuv420P(),
|
PixelFormat.YUV420P => new PixelFormatYuv420P(),
|
||||||
@@ -15,9 +15,9 @@ public static class AvailablePixelFormats
|
|||||||
_ => LogUnknownPixelFormat(pixelFormat, logger)
|
_ => LogUnknownPixelFormat(pixelFormat, logger)
|
||||||
};
|
};
|
||||||
|
|
||||||
private static Option<IPixelFormat> LogUnknownPixelFormat(string pixelFormat, ILogger logger)
|
private static Option<IPixelFormat> 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<IPixelFormat>.None;
|
return Option<IPixelFormat>.None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ public class CustomQueryParser : QueryParser
|
|||||||
SearchIndex.HeightField,
|
SearchIndex.HeightField,
|
||||||
SearchIndex.WidthField,
|
SearchIndex.WidthField,
|
||||||
SearchIndex.SeasonNumberField,
|
SearchIndex.SeasonNumberField,
|
||||||
SearchIndex.EpisodeNumberField
|
SearchIndex.EpisodeNumberField,
|
||||||
|
SearchIndex.VideoBitDepthField
|
||||||
};
|
};
|
||||||
|
|
||||||
public CustomQueryParser(LuceneVersion matchVersion, string f, Analyzer a) : base(matchVersion, f, a)
|
public CustomQueryParser(LuceneVersion matchVersion, string f, Analyzer a) : base(matchVersion, f, a)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ using ErsatzTV.Core.Interfaces.Repositories;
|
|||||||
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Core.Search;
|
using ErsatzTV.Core.Search;
|
||||||
|
using ErsatzTV.FFmpeg;
|
||||||
|
using ErsatzTV.FFmpeg.Format;
|
||||||
using LanguageExt.UnsafeValueAccess;
|
using LanguageExt.UnsafeValueAccess;
|
||||||
using Lucene.Net.Analysis;
|
using Lucene.Net.Analysis;
|
||||||
using Lucene.Net.Analysis.Core;
|
using Lucene.Net.Analysis.Core;
|
||||||
@@ -20,6 +22,7 @@ using Lucene.Net.Store;
|
|||||||
using Lucene.Net.Util;
|
using Lucene.Net.Util;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Directory = System.IO.Directory;
|
using Directory = System.IO.Directory;
|
||||||
|
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
|
||||||
using Query = Lucene.Net.Search.Query;
|
using Query = Lucene.Net.Search.Query;
|
||||||
|
|
||||||
namespace ErsatzTV.Infrastructure.Search;
|
namespace ErsatzTV.Infrastructure.Search;
|
||||||
@@ -56,6 +59,8 @@ public sealed class SearchIndex : ISearchIndex
|
|||||||
private const string ShowGenreField = "show_genre";
|
private const string ShowGenreField = "show_genre";
|
||||||
private const string ShowTagField = "show_tag";
|
private const string ShowTagField = "show_tag";
|
||||||
private const string MetadataKindField = "metadata_kind";
|
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 MinutesField = "minutes";
|
||||||
internal const string HeightField = "height";
|
internal const string HeightField = "height";
|
||||||
@@ -64,6 +69,7 @@ public sealed class SearchIndex : ISearchIndex
|
|||||||
internal const string EpisodeNumberField = "episode_number";
|
internal const string EpisodeNumberField = "episode_number";
|
||||||
internal const string AddedDateField = "added_date";
|
internal const string AddedDateField = "added_date";
|
||||||
internal const string ReleaseDateField = "release_date";
|
internal const string ReleaseDateField = "release_date";
|
||||||
|
internal const string VideoBitDepthField = "video_bit_depth";
|
||||||
|
|
||||||
public const string MovieType = "movie";
|
public const string MovieType = "movie";
|
||||||
public const string ShowType = "show";
|
public const string ShowType = "show";
|
||||||
@@ -73,7 +79,7 @@ public sealed class SearchIndex : ISearchIndex
|
|||||||
public const string EpisodeType = "episode";
|
public const string EpisodeType = "episode";
|
||||||
public const string OtherVideoType = "other_video";
|
public const string OtherVideoType = "other_video";
|
||||||
public const string SongType = "song";
|
public const string SongType = "song";
|
||||||
|
|
||||||
private readonly List<CultureInfo> _cultureInfos;
|
private readonly List<CultureInfo> _cultureInfos;
|
||||||
|
|
||||||
private readonly ILogger<SearchIndex> _logger;
|
private readonly ILogger<SearchIndex> _logger;
|
||||||
@@ -89,7 +95,7 @@ public sealed class SearchIndex : ISearchIndex
|
|||||||
_initialized = false;
|
_initialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Version => 33;
|
public int Version => 34;
|
||||||
|
|
||||||
public async Task<bool> Initialize(
|
public async Task<bool> Initialize(
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem,
|
||||||
@@ -382,14 +388,7 @@ public sealed class SearchIndex : ISearchIndex
|
|||||||
|
|
||||||
await AddLanguages(searchRepository, doc, movie.MediaVersions);
|
await AddLanguages(searchRepository, doc, movie.MediaVersions);
|
||||||
|
|
||||||
foreach (MediaVersion version in movie.MediaVersions.HeadOrNone())
|
AddStatistics(doc, movie.MediaVersions);
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(metadata.ContentRating))
|
if (!string.IsNullOrWhiteSpace(metadata.ContentRating))
|
||||||
{
|
{
|
||||||
@@ -752,14 +751,7 @@ public sealed class SearchIndex : ISearchIndex
|
|||||||
|
|
||||||
await AddLanguages(searchRepository, doc, musicVideo.MediaVersions);
|
await AddLanguages(searchRepository, doc, musicVideo.MediaVersions);
|
||||||
|
|
||||||
foreach (MediaVersion version in musicVideo.MediaVersions.HeadOrNone())
|
AddStatistics(doc, musicVideo.MediaVersions);
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.ReleaseDate.HasValue)
|
if (metadata.ReleaseDate.HasValue)
|
||||||
{
|
{
|
||||||
@@ -891,14 +883,7 @@ public sealed class SearchIndex : ISearchIndex
|
|||||||
|
|
||||||
await AddLanguages(searchRepository, doc, episode.MediaVersions);
|
await AddLanguages(searchRepository, doc, episode.MediaVersions);
|
||||||
|
|
||||||
foreach (MediaVersion version in episode.MediaVersions.HeadOrNone())
|
AddStatistics(doc, episode.MediaVersions);
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.ReleaseDate.HasValue)
|
if (metadata.ReleaseDate.HasValue)
|
||||||
{
|
{
|
||||||
@@ -988,14 +973,7 @@ public sealed class SearchIndex : ISearchIndex
|
|||||||
|
|
||||||
await AddLanguages(searchRepository, doc, otherVideo.MediaVersions);
|
await AddLanguages(searchRepository, doc, otherVideo.MediaVersions);
|
||||||
|
|
||||||
foreach (MediaVersion version in otherVideo.MediaVersions.HeadOrNone())
|
AddStatistics(doc, otherVideo.MediaVersions);
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(metadata.ContentRating))
|
if (!string.IsNullOrWhiteSpace(metadata.ContentRating))
|
||||||
{
|
{
|
||||||
@@ -1089,11 +1067,7 @@ public sealed class SearchIndex : ISearchIndex
|
|||||||
|
|
||||||
await AddLanguages(searchRepository, doc, song.MediaVersions);
|
await AddLanguages(searchRepository, doc, song.MediaVersions);
|
||||||
|
|
||||||
foreach (MediaVersion version in song.MediaVersions.HeadOrNone())
|
AddStatistics(doc, song.MediaVersions);
|
||||||
{
|
|
||||||
doc.Add(
|
|
||||||
new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), 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));
|
||||||
|
|
||||||
@@ -1152,6 +1126,42 @@ public sealed class SearchIndex : ISearchIndex
|
|||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private 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));
|
||||||
|
|
||||||
|
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<IPixelFormat> 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)
|
private static void AddMetadataGuids(Metadata metadata, Document doc)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ The following fields are available for searching movies:
|
|||||||
- `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
|
- `height`: The movie height
|
||||||
- `width`: The movie width
|
- `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`
|
- `type`: Always `movie`
|
||||||
|
|
||||||
### Shows
|
### 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_title`: The title of the show that contains the episode
|
||||||
- `show_genre`: The genre 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
|
- `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`
|
- `type`: Always `episode`
|
||||||
|
|
||||||
### Artists
|
### 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
|
- `minutes`: The rounded-up whole number duration of the music video in minutes
|
||||||
- `height`: The music video height
|
- `height`: The music video height
|
||||||
- `width`: The music video width
|
- `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`
|
- `type`: Always `music_video`
|
||||||
|
|
||||||
### Other Videos
|
### 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
|
- `minutes`: The rounded-up whole number duration of the video in minutes
|
||||||
- `height`: The video height
|
- `height`: The video height
|
||||||
- `width`: The video width
|
- `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`
|
- `type`: Always `other_video`
|
||||||
|
|
||||||
### Songs
|
### Songs
|
||||||
|
|||||||
Reference in New Issue
Block a user