Browse Source

bug fixes and search tweaks (#781)

* fix movie nfo processing

* fix local movie fallback metadata

* use imagesharp again

* fix search edge case

* add show_genre and show_tag to search index
pull/782/head
Jason Dove 3 years ago committed by GitHub
parent
commit
e9be182bed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      CHANGELOG.md
  2. 2
      ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs
  3. 4
      ErsatzTV.Core/Interfaces/Images/IImageCache.cs
  4. 3
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  5. 57
      ErsatzTV.Core/Metadata/LocalFolderScanner.cs
  6. 4
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  7. 18
      ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs
  8. 2
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  9. 41
      ErsatzTV.Infrastructure/Images/ImageCache.cs
  10. 4
      ErsatzTV.Infrastructure/Search/CustomMultiFieldQueryParser.cs
  11. 4
      ErsatzTV.Infrastructure/Search/CustomQueryParser.cs
  12. 36
      ErsatzTV.Infrastructure/Search/SearchIndex.cs
  13. 4
      docs/user-guide/search.md

7
CHANGELOG.md

@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Fixed
- Fix processing local movie NFO metadata without a `year` value
- Fix processing local movie fallback metadata
- Fix search edge case where very recently added items (hours) would not be returned by relative date queries
### Added
- Add `show_genre` and `show_tag` to search index for seasons and episodes
## [0.5.5-beta] - 2022-05-03
### Fixed

2
ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs

@ -193,7 +193,7 @@ public class SongVideoGenerator : ISongVideoGenerator @@ -193,7 +193,7 @@ public class SongVideoGenerator : ISongVideoGenerator
{
string hash = hashes[NextRandom(hashes.Count)];
backgroundPath = _imageCache.WriteBlurHash(hash, channel.FFmpegProfile.Resolution);
backgroundPath = await _imageCache.WriteBlurHash(hash, channel.FFmpegProfile.Resolution);
videoVersion.Height = channel.FFmpegProfile.Resolution.Height;
videoVersion.Width = channel.FFmpegProfile.Resolution.Width;

4
ErsatzTV.Core/Interfaces/Images/IImageCache.cs

@ -8,6 +8,6 @@ public interface IImageCache @@ -8,6 +8,6 @@ public interface IImageCache
Task<Either<BaseError, string>> SaveArtworkToCache(Stream stream, ArtworkKind artworkKind);
Task<Either<BaseError, string>> CopyArtworkToCache(string path, ArtworkKind artworkKind);
string GetPathForImage(string fileName, ArtworkKind artworkKind, Option<int> maybeMaxHeight);
string CalculateBlurHash(string fileName, ArtworkKind artworkKind, int x, int y);
string WriteBlurHash(string blurHash, IDisplaySize targetSize);
Task<string> CalculateBlurHash(string fileName, ArtworkKind artworkKind, int x, int y);
Task<string> WriteBlurHash(string blurHash, IDisplaySize targetSize);
}

3
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -70,7 +70,8 @@ public class FallbackMetadataProvider : IFallbackMetadataProvider @@ -70,7 +70,8 @@ public class FallbackMetadataProvider : IFallbackMetadataProvider
Studios = new List<Studio>(),
Actors = new List<Actor>(),
Directors = new List<Director>(),
Writers = new List<Writer>()
Writers = new List<Writer>(),
Guids = new List<MetadataGuid>()
};
return fileName != null ? GetMovieMetadata(fileName, metadata) : metadata;

57
ErsatzTV.Core/Metadata/LocalFolderScanner.cs

@ -95,20 +95,23 @@ public abstract class LocalFolderScanner @@ -95,20 +95,23 @@ public abstract class LocalFolderScanner
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", path);
Either<BaseError, bool> refreshResult =
await _localStatisticsProvider.RefreshStatistics(ffmpegPath, ffprobePath, mediaItem.Item);
refreshResult.Match(
result =>
{
if (result)
foreach (BaseError error in refreshResult.LeftToSeq())
{
mediaItem.IsUpdated = true;
}
},
error =>
_logger.LogWarning(
"Unable to refresh {Attribute} for media item {Path}. Error: {Error}",
"Statistics",
path,
error.Value));
error.Value);
}
foreach (bool result in refreshResult.RightToSeq())
{
if (result)
{
mediaItem.IsUpdated = true;
}
}
}
return mediaItem;
@ -200,9 +203,21 @@ public abstract class LocalFolderScanner @@ -200,9 +203,21 @@ public abstract class LocalFolderScanner
if (metadata is SongMetadata)
{
artwork.BlurHash43 = _imageCache.CalculateBlurHash(cacheName, artworkKind, 4, 3);
artwork.BlurHash54 = _imageCache.CalculateBlurHash(cacheName, artworkKind, 5, 4);
artwork.BlurHash64 = _imageCache.CalculateBlurHash(cacheName, artworkKind, 6, 4);
artwork.BlurHash43 = await _imageCache.CalculateBlurHash(
cacheName,
artworkKind,
4,
3);
artwork.BlurHash54 = await _imageCache.CalculateBlurHash(
cacheName,
artworkKind,
5,
4);
artwork.BlurHash64 = await _imageCache.CalculateBlurHash(
cacheName,
artworkKind,
6,
4);
}
await _metadataRepository.UpdateArtworkPath(artwork);
@ -220,9 +235,21 @@ public abstract class LocalFolderScanner @@ -220,9 +235,21 @@ public abstract class LocalFolderScanner
if (metadata is SongMetadata)
{
artwork.BlurHash43 = _imageCache.CalculateBlurHash(cacheName, artworkKind, 4, 3);
artwork.BlurHash54 = _imageCache.CalculateBlurHash(cacheName, artworkKind, 5, 4);
artwork.BlurHash64 = _imageCache.CalculateBlurHash(cacheName, artworkKind, 6, 4);
artwork.BlurHash43 = await _imageCache.CalculateBlurHash(
cacheName,
artworkKind,
4,
3);
artwork.BlurHash54 = await _imageCache.CalculateBlurHash(
cacheName,
artworkKind,
5,
4);
artwork.BlurHash64 = await _imageCache.CalculateBlurHash(
cacheName,
artworkKind,
6,
4);
}
metadata.Artwork.Add(artwork);

4
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -1019,7 +1019,9 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -1019,7 +1019,9 @@ public class LocalMetadataProvider : ILocalMetadataProvider
year = nfo.Year;
}
DateTime releaseDate = new DateTimeOffset(year, 0, 0, 0, 0, 0, TimeSpan.Zero).UtcDateTime;
DateTime releaseDate = year > 0
? new DateTimeOffset(year, 0, 0, 0, 0, 0, TimeSpan.Zero).UtcDateTime
: SystemTime.MinValueUtc;
foreach (DateTime premiered in nfo.Premiered)
{

18
ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs

@ -52,6 +52,11 @@ public class SearchRepository : ISearchRepository @@ -52,6 +52,11 @@ public class SearchRepository : ISearchRepository
.Include(mi => (mi as Episode).Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.ThenInclude(s => s.Genres)
.Include(mi => (mi as Episode).Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.ThenInclude(s => s.Tags)
.Include(mi => (mi as Season).SeasonMetadata)
.ThenInclude(sm => sm.Genres)
.Include(mi => (mi as Season).SeasonMetadata)
@ -62,6 +67,10 @@ public class SearchRepository : ISearchRepository @@ -62,6 +67,10 @@ public class SearchRepository : ISearchRepository
.ThenInclude(sm => sm.Actors)
.Include(mi => (mi as Season).Show)
.ThenInclude(sm => sm.ShowMetadata)
.ThenInclude(sm => sm.Genres)
.Include(mi => (mi as Season).Show)
.ThenInclude(sm => sm.ShowMetadata)
.ThenInclude(sm => sm.Tags)
.Include(mi => (mi as Show).ShowMetadata)
.ThenInclude(mm => mm.Genres)
.Include(mi => (mi as Show).ShowMetadata)
@ -187,6 +196,11 @@ public class SearchRepository : ISearchRepository @@ -187,6 +196,11 @@ public class SearchRepository : ISearchRepository
.Include(mi => (mi as Episode).Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.ThenInclude(s => s.Genres)
.Include(mi => (mi as Episode).Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.ThenInclude(s => s.Tags)
.Include(mi => (mi as Season).SeasonMetadata)
.ThenInclude(sm => sm.Genres)
.Include(mi => (mi as Season).SeasonMetadata)
@ -197,6 +211,10 @@ public class SearchRepository : ISearchRepository @@ -197,6 +211,10 @@ public class SearchRepository : ISearchRepository
.ThenInclude(sm => sm.Actors)
.Include(mi => (mi as Season).Show)
.ThenInclude(sm => sm.ShowMetadata)
.ThenInclude(sm => sm.Genres)
.Include(mi => (mi as Season).Show)
.ThenInclude(sm => sm.ShowMetadata)
.ThenInclude(sm => sm.Tags)
.Include(mi => (mi as Show).ShowMetadata)
.ThenInclude(mm => mm.Genres)
.Include(mi => (mi as Show).ShowMetadata)

2
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blurhash.System.Drawing.Common" Version="2.2.0" />
<PackageReference Include="Blurhash.ImageSharp" Version="3.0.0" />
<PackageReference Include="CliWrap" Version="3.4.4" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00016" />

41
ErsatzTV.Infrastructure/Images/ImageCache.cs

@ -1,15 +1,15 @@ @@ -1,15 +1,15 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.Security.Cryptography;
using System.Security.Cryptography;
using System.Text;
using Blurhash.ImageSharp;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using Decoder = System.Drawing.Common.Blurhash.Decoder;
using Encoder = System.Drawing.Common.Blurhash.Encoder;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace ErsatzTV.Infrastructure.Images;
@ -116,18 +116,31 @@ public class ImageCache : IImageCache @@ -116,18 +116,31 @@ public class ImageCache : IImageCache
return Path.Combine(baseFolder, fileName);
}
public string CalculateBlurHash(string fileName, ArtworkKind artworkKind, int x, int y)
public async Task<string> CalculateBlurHash(string fileName, ArtworkKind artworkKind, int x, int y)
{
var encoder = new Encoder();
string targetFile = GetPathForImage(fileName, artworkKind, Option<int>.None);
// ReSharper disable once ConvertToUsingDeclaration
using (var image = Image.FromFile(targetFile))
using (FileStream fs = File.OpenRead(targetFile))
{
using (var image = await Image.LoadAsync<Rgba32>(fs))
{
// resize before calculating blur hash; it doesn't need giant images
if (image.Height > 200)
{
image.Mutate(i => i.Resize(0, 200));
}
else if (image.Width > 200)
{
return encoder.Encode(image, x, y);
image.Mutate(i => i.Resize(200, 0));
}
return Blurhasher.Encode(image, x, y);
}
}
}
public string WriteBlurHash(string blurHash, IDisplaySize targetSize)
public async Task<string> WriteBlurHash(string blurHash, IDisplaySize targetSize)
{
byte[] bytes = Encoding.UTF8.GetBytes(blurHash);
string base64 = Convert.ToBase64String(bytes).Replace("+", "_").Replace("/", "-").Replace("=", "");
@ -137,11 +150,13 @@ public class ImageCache : IImageCache @@ -137,11 +150,13 @@ public class ImageCache : IImageCache
string folder = Path.GetDirectoryName(targetFile);
_localFileSystem.EnsureFolderExists(folder);
var decoder = new Decoder();
// ReSharper disable once ConvertToUsingDeclaration
using (Image image = decoder.Decode(blurHash, targetSize.Width, targetSize.Height))
using (FileStream fs = File.OpenWrite(targetFile))
{
image.Save(targetFile, ImageFormat.Png);
using (Image<Rgb24> image = Blurhasher.Decode(blurHash, targetSize.Width, targetSize.Height))
{
await image.SaveAsPngAsync(fs);
}
}
}

4
ErsatzTV.Infrastructure/Search/CustomMultiFieldQueryParser.cs

@ -46,7 +46,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser @@ -46,7 +46,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
{
if (field == "released_inthelast" && CustomQueryParser.ParseStart(queryText, out DateTime start))
{
var todayString = DateTime.Today.ToString("yyyyMMdd");
var todayString = DateTime.UtcNow.ToString("yyyyMMdd");
var dateString = start.ToString("yyyyMMdd");
return base.GetRangeQuery("release_date", dateString, todayString, true, true);
@ -61,7 +61,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser @@ -61,7 +61,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
if (field == "added_inthelast" && CustomQueryParser.ParseStart(queryText, out DateTime addedStart))
{
var todayString = DateTime.Today.ToString("yyyyMMdd");
var todayString = DateTime.UtcNow.ToString("yyyyMMdd");
var dateString = addedStart.ToString("yyyyMMdd");
return base.GetRangeQuery("added_date", dateString, todayString, true, true);

4
ErsatzTV.Infrastructure/Search/CustomQueryParser.cs

@ -53,7 +53,7 @@ public class CustomQueryParser : QueryParser @@ -53,7 +53,7 @@ public class CustomQueryParser : QueryParser
{
if (field == "released_inthelast" && ParseStart(queryText, out DateTime start))
{
var todayString = DateTime.Today.ToString("yyyyMMdd");
var todayString = DateTime.UtcNow.ToString("yyyyMMdd");
var dateString = start.ToString("yyyyMMdd");
return base.GetRangeQuery("release_date", dateString, todayString, true, true);
@ -68,7 +68,7 @@ public class CustomQueryParser : QueryParser @@ -68,7 +68,7 @@ public class CustomQueryParser : QueryParser
if (field == "added_inthelast" && ParseStart(queryText, out DateTime addedStart))
{
var todayString = DateTime.Today.ToString("yyyyMMdd");
var todayString = DateTime.UtcNow.ToString("yyyyMMdd");
var dateString = addedStart.ToString("yyyyMMdd");
return base.GetRangeQuery("added_date", dateString, todayString, true, true);

36
ErsatzTV.Infrastructure/Search/SearchIndex.cs

@ -54,6 +54,8 @@ public sealed class SearchIndex : ISearchIndex @@ -54,6 +54,8 @@ public sealed class SearchIndex : ISearchIndex
private const string StateField = "state";
private const string AlbumArtistField = "album_artist";
private const string ShowTitleField = "show_title";
private const string ShowGenreField = "show_genre";
private const string ShowTagField = "show_tag";
internal const string MinutesField = "minutes";
internal const string HeightField = "height";
@ -85,7 +87,7 @@ public sealed class SearchIndex : ISearchIndex @@ -85,7 +87,7 @@ public sealed class SearchIndex : ISearchIndex
_initialized = false;
}
public int Version => 23;
public int Version => 24;
public async Task<bool> Initialize(
ILocalFileSystem localFileSystem,
@ -585,9 +587,20 @@ public sealed class SearchIndex : ISearchIndex @@ -585,9 +587,20 @@ public sealed class SearchIndex : ISearchIndex
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, season.Show?.ShowMetadata.Head().Title, Field.Store.NO)
new TextField(ShowTitleField, showMetadata.Title, Field.Store.NO)
};
// add some show fields to help filter shows 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));
}
List<string> languages = await searchRepository.GetLanguagesForSeason(season);
await AddLanguages(searchRepository, doc, languages);
@ -797,10 +810,25 @@ public sealed class SearchIndex : ISearchIndex @@ -797,10 +810,25 @@ public sealed class SearchIndex : ISearchIndex
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(ShowTitleField, episode.Season?.Show?.ShowMetadata.Head().Title, Field.Store.NO)
new Int32Field(EpisodeNumberField, metadata.EpisodeNumber, 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));
}
}
if (!string.IsNullOrWhiteSpace(metadata.Title))
{
doc.Add(new TextField(TitleField, metadata.Title, Field.Store.NO));

4
docs/user-guide/search.md

@ -55,6 +55,8 @@ The following fields are available for searching seasons: @@ -55,6 +55,8 @@ The following fields are available for searching seasons:
- `library_name`: The name of the library that contains the season
- `season_number`: The season number
- `show_title`: The title of the show that contains the season
- `show_genre`: The genre of the show that contains the season
- `show_tag`: The tag of the show that contains the season
- `type`: Always `season`
### Episodes
@ -75,6 +77,8 @@ The following fields are available for searching episodes: @@ -75,6 +77,8 @@ The following fields are available for searching episodes:
- `season_number`: The episode season number
- `episode_number`: The episode number
- `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
- `type`: Always `episode`
### Artists

Loading…
Cancel
Save