Browse Source

support 10-bit content with nvidia acceleration (#273)

* use ffprobe for plex statistics

* emby and jellyfin respect library refresh interval

* support 10-bit content with nvidia acceleration
pull/275/head
Jason Dove 5 years ago committed by GitHub
parent
commit
1a7e6dda54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      CHANGELOG.md
  2. 15
      ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs
  3. 15
      ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs
  4. 24
      ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs
  5. 2
      ErsatzTV.Core/Domain/MediaItem/MediaStream.cs
  6. 11
      ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs
  7. 6
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  8. 2
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  9. 3
      ErsatzTV.Core/Interfaces/Plex/IPlexMovieLibraryScanner.cs
  10. 3
      ErsatzTV.Core/Interfaces/Plex/IPlexTelevisionLibraryScanner.cs
  11. 10
      ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs
  12. 65
      ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs
  13. 126
      ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs
  14. 12
      ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs
  15. 2976
      ErsatzTV.Infrastructure/Migrations/20210619191334_Add_MediaStreamPixelFormat.Designer.cs
  16. 34
      ErsatzTV.Infrastructure/Migrations/20210619191334_Add_MediaStreamPixelFormat.cs
  17. 2976
      ErsatzTV.Infrastructure/Migrations/20210620012739_Reset_AllStatistics.Designer.cs
  18. 27
      ErsatzTV.Infrastructure/Migrations/20210620012739_Reset_AllStatistics.cs
  19. 6
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

10
CHANGELOG.md

@ -4,6 +4,16 @@ 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/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added
- Store pixel format with media statistics; this is needed to support normalization of 10-bit media items
- This requires re-ingesting statistics for all media items the first time this version is launched
### Changed
- Use ffprobe to retrieve statistics for Plex media items (Local, Emby and Jellyfin libraries already use ffprobe)
### Fixed
- Fix playback of transcoded 10-bit media items (pixel format `yuv420p10le`) on Nvidia hardware
- Emby and Jellyfin scanners now respect library refresh interval setting
## [0.0.47-prealpha] - 2021-06-15 ## [0.0.47-prealpha] - 2021-06-15
### Added ### Added

15
ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs

@ -68,7 +68,8 @@ namespace ErsatzTV.Application.Emby.Commands
private async Task<Unit> Synchronize(RequestParameters parameters) private async Task<Unit> Synchronize(RequestParameters parameters)
{ {
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? DateTime.MinValue, TimeSpan.Zero); var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
if (parameters.ForceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6)) DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
{ {
switch (parameters.Library.MediaKind) switch (parameters.Library.MediaKind)
{ {
@ -104,12 +105,14 @@ namespace ErsatzTV.Application.Emby.Commands
private async Task<Validation<BaseError, RequestParameters>> Validate( private async Task<Validation<BaseError, RequestParameters>> Validate(
ISynchronizeEmbyLibraryById request) => ISynchronizeEmbyLibraryById request) =>
(await ValidateConnection(request), await EmbyLibraryMustExist(request), await ValidateFFprobePath()) (await ValidateConnection(request), await EmbyLibraryMustExist(request),
await ValidateLibraryRefreshInterval(), await ValidateFFprobePath())
.Apply( .Apply(
(connectionParameters, embyLibrary, ffprobePath) => new RequestParameters( (connectionParameters, embyLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters(
connectionParameters, connectionParameters,
embyLibrary, embyLibrary,
request.ForceScan, request.ForceScan,
libraryRefreshInterval,
ffprobePath ffprobePath
)); ));
@ -149,6 +152,11 @@ namespace ErsatzTV.Application.Emby.Commands
_mediaSourceRepository.GetEmbyLibrary(request.EmbyLibraryId) _mediaSourceRepository.GetEmbyLibrary(request.EmbyLibraryId)
.Map(v => v.ToValidation<BaseError>($"Emby library {request.EmbyLibraryId} does not exist.")); .Map(v => v.ToValidation<BaseError>($"Emby library {request.EmbyLibraryId} does not exist."));
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.FilterT(lri => lri > 0)
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
private Task<Validation<BaseError, string>> ValidateFFprobePath() => private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath) _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists) .FilterT(File.Exists)
@ -160,6 +168,7 @@ namespace ErsatzTV.Application.Emby.Commands
ConnectionParameters ConnectionParameters, ConnectionParameters ConnectionParameters,
EmbyLibrary Library, EmbyLibrary Library,
bool ForceScan, bool ForceScan,
int LibraryRefreshInterval,
string FFprobePath); string FFprobePath);
private record ConnectionParameters( private record ConnectionParameters(

15
ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs

@ -68,7 +68,8 @@ namespace ErsatzTV.Application.Jellyfin.Commands
private async Task<Unit> Synchronize(RequestParameters parameters) private async Task<Unit> Synchronize(RequestParameters parameters)
{ {
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? DateTime.MinValue, TimeSpan.Zero); var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
if (parameters.ForceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6)) DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
{ {
switch (parameters.Library.MediaKind) switch (parameters.Library.MediaKind)
{ {
@ -104,12 +105,14 @@ namespace ErsatzTV.Application.Jellyfin.Commands
private async Task<Validation<BaseError, RequestParameters>> Validate( private async Task<Validation<BaseError, RequestParameters>> Validate(
ISynchronizeJellyfinLibraryById request) => ISynchronizeJellyfinLibraryById request) =>
(await ValidateConnection(request), await JellyfinLibraryMustExist(request), await ValidateFFprobePath()) (await ValidateConnection(request), await JellyfinLibraryMustExist(request),
await ValidateLibraryRefreshInterval(), await ValidateFFprobePath())
.Apply( .Apply(
(connectionParameters, jellyfinLibrary, ffprobePath) => new RequestParameters( (connectionParameters, jellyfinLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters(
connectionParameters, connectionParameters,
jellyfinLibrary, jellyfinLibrary,
request.ForceScan, request.ForceScan,
libraryRefreshInterval,
ffprobePath ffprobePath
)); ));
@ -149,6 +152,11 @@ namespace ErsatzTV.Application.Jellyfin.Commands
_mediaSourceRepository.GetJellyfinLibrary(request.JellyfinLibraryId) _mediaSourceRepository.GetJellyfinLibrary(request.JellyfinLibraryId)
.Map(v => v.ToValidation<BaseError>($"Jellyfin library {request.JellyfinLibraryId} does not exist.")); .Map(v => v.ToValidation<BaseError>($"Jellyfin library {request.JellyfinLibraryId} does not exist."));
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.FilterT(lri => lri > 0)
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
private Task<Validation<BaseError, string>> ValidateFFprobePath() => private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath) _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists) .FilterT(File.Exists)
@ -160,6 +168,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
ConnectionParameters ConnectionParameters, ConnectionParameters ConnectionParameters,
JellyfinLibrary Library, JellyfinLibrary Library,
bool ForceScan, bool ForceScan,
int LibraryRefreshInterval,
string FFprobePath); string FFprobePath);
private record ConnectionParameters( private record ConnectionParameters(

24
ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs

@ -1,4 +1,5 @@
using System; using System;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -74,13 +75,15 @@ namespace ErsatzTV.Application.Plex.Commands
await _plexMovieLibraryScanner.ScanLibrary( await _plexMovieLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection, parameters.ConnectionParameters.ActiveConnection,
parameters.ConnectionParameters.PlexServerAuthToken, parameters.ConnectionParameters.PlexServerAuthToken,
parameters.Library); parameters.Library,
parameters.FFprobePath);
break; break;
case LibraryMediaKind.Shows: case LibraryMediaKind.Shows:
await _plexTelevisionLibraryScanner.ScanLibrary( await _plexTelevisionLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection, parameters.ConnectionParameters.ActiveConnection,
parameters.ConnectionParameters.PlexServerAuthToken, parameters.ConnectionParameters.PlexServerAuthToken,
parameters.Library); parameters.Library,
parameters.FFprobePath);
break; break;
} }
@ -100,13 +103,14 @@ namespace ErsatzTV.Application.Plex.Commands
private async Task<Validation<BaseError, RequestParameters>> Validate(ISynchronizePlexLibraryById request) => private async Task<Validation<BaseError, RequestParameters>> Validate(ISynchronizePlexLibraryById request) =>
(await ValidateConnection(request), await PlexLibraryMustExist(request), (await ValidateConnection(request), await PlexLibraryMustExist(request),
await ValidateLibraryRefreshInterval()) await ValidateLibraryRefreshInterval(), await ValidateFFprobePath())
.Apply( .Apply(
(connectionParameters, plexLibrary, libraryRefreshInterval) => new RequestParameters( (connectionParameters, plexLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters(
connectionParameters, connectionParameters,
plexLibrary, plexLibrary,
request.ForceScan, request.ForceScan,
libraryRefreshInterval libraryRefreshInterval,
ffprobePath
)); ));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection( private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
@ -149,12 +153,20 @@ namespace ErsatzTV.Application.Plex.Commands
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval) _configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.FilterT(lri => lri > 0) .FilterT(lri => lri > 0)
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid")); .Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists)
.Map(
ffprobePath =>
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private record RequestParameters( private record RequestParameters(
ConnectionParameters ConnectionParameters, ConnectionParameters ConnectionParameters,
PlexLibrary Library, PlexLibrary Library,
bool ForceScan, bool ForceScan,
int LibraryRefreshInterval); int LibraryRefreshInterval,
string FFprobePath);
private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection) private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection)
{ {

2
ErsatzTV.Core/Domain/MediaItem/MediaStream.cs

@ -12,6 +12,8 @@
public string Title { get; set; } public string Title { get; set; }
public bool Default { get; set; } public bool Default { get; set; }
public bool Forced { get; set; } public bool Forced { get; set; }
public string PixelFormat { get; set; }
public int BitsPerRawSample { get; set; }
public int MediaVersionId { get; set; } public int MediaVersionId { get; set; }
public MediaVersion MediaVersion { get; set; } public MediaVersion MediaVersion { get; set; }
} }

11
ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs

@ -20,6 +20,7 @@ namespace ErsatzTV.Core.FFmpeg
private IDisplaySize _resolution; private IDisplaySize _resolution;
private Option<IDisplaySize> _scaleToSize = None; private Option<IDisplaySize> _scaleToSize = None;
private Option<ChannelWatermark> _watermark; private Option<ChannelWatermark> _watermark;
private string _pixelFormat;
public FFmpegComplexFilterBuilder WithHardwareAcceleration(HardwareAccelerationKind hardwareAccelerationKind) public FFmpegComplexFilterBuilder WithHardwareAcceleration(HardwareAccelerationKind hardwareAccelerationKind)
{ {
@ -63,6 +64,12 @@ namespace ErsatzTV.Core.FFmpeg
return this; return this;
} }
public FFmpegComplexFilterBuilder WithInputPixelFormat(string pixelFormat)
{
_pixelFormat = pixelFormat;
return this;
}
public FFmpegComplexFilterBuilder WithWatermark(Option<ChannelWatermark> watermark, IDisplaySize resolution) public FFmpegComplexFilterBuilder WithWatermark(Option<ChannelWatermark> watermark, IDisplaySize resolution)
{ {
_watermark = watermark; _watermark = watermark;
@ -128,6 +135,8 @@ namespace ErsatzTV.Core.FFmpeg
string filter = acceleration switch string filter = acceleration switch
{ {
HardwareAccelerationKind.Qsv => $"scale_qsv=w={size.Width}:h={size.Height}", HardwareAccelerationKind.Qsv => $"scale_qsv=w={size.Width}:h={size.Height}",
HardwareAccelerationKind.Nvenc when _pixelFormat == "yuv420p10le" =>
$"hwdownload,format=p010le,format=nv12,hwupload,scale_npp={size.Width}:{size.Height}",
HardwareAccelerationKind.Nvenc => $"scale_npp={size.Width}:{size.Height}", HardwareAccelerationKind.Nvenc => $"scale_npp={size.Width}:{size.Height}",
HardwareAccelerationKind.Vaapi => $"scale_vaapi=w={size.Width}:h={size.Height}", HardwareAccelerationKind.Vaapi => $"scale_vaapi=w={size.Width}:h={size.Height}",
_ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear" _ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear"
@ -150,6 +159,8 @@ namespace ErsatzTV.Core.FFmpeg
string format = acceleration switch string format = acceleration switch
{ {
HardwareAccelerationKind.Vaapi => "format=nv12|vaapi", HardwareAccelerationKind.Vaapi => "format=nv12|vaapi",
HardwareAccelerationKind.Nvenc when _scaleToSize.IsNone && _pixelFormat == "yuv420p10le" =>
"format=p010le,format=nv12",
_ => "format=nv12" _ => "format=nv12"
}; };
videoFilterQueue.Add(format); videoFilterQueue.Add(format);

6
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -175,7 +175,7 @@ namespace ErsatzTV.Core.FFmpeg
return this; return this;
} }
public FFmpegProcessBuilder WithInputCodec(string input, HardwareAccelerationKind hwAccel, string codec) public FFmpegProcessBuilder WithInputCodec(string input, HardwareAccelerationKind hwAccel, string codec, string pixelFormat)
{ {
if (hwAccel == HardwareAccelerationKind.Qsv && QsvMap.TryGetValue(codec, out string qsvCodec)) if (hwAccel == HardwareAccelerationKind.Qsv && QsvMap.TryGetValue(codec, out string qsvCodec))
{ {
@ -183,7 +183,9 @@ namespace ErsatzTV.Core.FFmpeg
_arguments.Add(qsvCodec); _arguments.Add(qsvCodec);
} }
_complexFilterBuilder = _complexFilterBuilder.WithInputCodec(codec); _complexFilterBuilder = _complexFilterBuilder
.WithInputCodec(codec)
.WithInputPixelFormat(pixelFormat);
_arguments.Add("-i"); _arguments.Add("-i");
_arguments.Add($"{input}"); _arguments.Add($"{input}");

2
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -62,7 +62,7 @@ namespace ErsatzTV.Core.FFmpeg
.WithFormatFlags(playbackSettings.FormatFlags) .WithFormatFlags(playbackSettings.FormatFlags)
.WithRealtimeOutput(playbackSettings.RealtimeOutput) .WithRealtimeOutput(playbackSettings.RealtimeOutput)
.WithSeek(playbackSettings.StreamSeek) .WithSeek(playbackSettings.StreamSeek)
.WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec) .WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec, videoStream.PixelFormat)
.WithWatermark(maybeWatermark, maybeWatermarkPath, channel.FFmpegProfile.Resolution, isAnimated) .WithWatermark(maybeWatermark, maybeWatermarkPath, channel.FFmpegProfile.Resolution, isAnimated)
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale) .WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)
.WithAlignedAudio(playbackSettings.AudioDuration) .WithAlignedAudio(playbackSettings.AudioDuration)

3
ErsatzTV.Core/Interfaces/Plex/IPlexMovieLibraryScanner.cs

@ -10,6 +10,7 @@ namespace ErsatzTV.Core.Interfaces.Plex
Task<Either<BaseError, Unit>> ScanLibrary( Task<Either<BaseError, Unit>> ScanLibrary(
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token, PlexServerAuthToken token,
PlexLibrary library); PlexLibrary library,
string ffprobePath);
} }
} }

3
ErsatzTV.Core/Interfaces/Plex/IPlexTelevisionLibraryScanner.cs

@ -10,6 +10,7 @@ namespace ErsatzTV.Core.Interfaces.Plex
Task<Either<BaseError, Unit>> ScanLibrary( Task<Either<BaseError, Unit>> ScanLibrary(
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token, PlexServerAuthToken token,
PlexLibrary library); PlexLibrary library,
string ffprobePath);
} }
} }

10
ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs

@ -194,9 +194,15 @@ namespace ErsatzTV.Core.Metadata
MediaStreamKind = MediaStreamKind.Video, MediaStreamKind = MediaStreamKind.Video,
Index = videoStream.index, Index = videoStream.index,
Codec = videoStream.codec_name, Codec = videoStream.codec_name,
Profile = (videoStream.profile ?? string.Empty).ToLowerInvariant() Profile = (videoStream.profile ?? string.Empty).ToLowerInvariant(),
PixelFormat = (videoStream.pix_fmt ?? string.Empty).ToLowerInvariant(),
}; };
if (int.TryParse(videoStream.bits_per_raw_sample, out int bitsPerRawSample))
{
stream.BitsPerRawSample = bitsPerRawSample;
}
if (videoStream.disposition is not null) if (videoStream.disposition is not null)
{ {
stream.Default = videoStream.disposition.@default == 1; stream.Default = videoStream.disposition.@default == 1;
@ -262,8 +268,10 @@ namespace ErsatzTV.Core.Metadata
int height, int height,
string sample_aspect_ratio, string sample_aspect_ratio,
string display_aspect_ratio, string display_aspect_ratio,
string pix_fmt,
string field_order, string field_order,
string r_frame_rate, string r_frame_rate,
string bits_per_raw_sample,
FFprobeDisposition disposition, FFprobeDisposition disposition,
FFProbeTags tags); FFProbeTags tags);
// ReSharper restore InconsistentNaming // ReSharper restore InconsistentNaming

65
ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs

@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
@ -18,6 +17,7 @@ namespace ErsatzTV.Core.Plex
public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScanner public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScanner
{ {
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILogger<PlexMovieLibraryScanner> _logger; private readonly ILogger<PlexMovieLibraryScanner> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMediator _mediator; private readonly IMediator _mediator;
@ -38,6 +38,7 @@ namespace ErsatzTV.Core.Plex
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
IPlexPathReplacementService plexPathReplacementService, IPlexPathReplacementService plexPathReplacementService,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILogger<PlexMovieLibraryScanner> logger) ILogger<PlexMovieLibraryScanner> logger)
: base(metadataRepository, logger) : base(metadataRepository, logger)
{ {
@ -50,13 +51,15 @@ namespace ErsatzTV.Core.Plex
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
_plexPathReplacementService = plexPathReplacementService; _plexPathReplacementService = plexPathReplacementService;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_localStatisticsProvider = localStatisticsProvider;
_logger = logger; _logger = logger;
} }
public async Task<Either<BaseError, Unit>> ScanLibrary( public async Task<Either<BaseError, Unit>> ScanLibrary(
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token, PlexServerAuthToken token,
PlexLibrary library) PlexLibrary library,
string ffprobePath)
{ {
List<PlexPathReplacement> pathReplacements = await _mediaSourceRepository List<PlexPathReplacement> pathReplacements = await _mediaSourceRepository
.GetPlexPathReplacements(library.MediaSourceId); .GetPlexPathReplacements(library.MediaSourceId);
@ -95,7 +98,7 @@ namespace ErsatzTV.Core.Plex
// TODO: figure out how to rebuild playlists // TODO: figure out how to rebuild playlists
Either<BaseError, MediaItemScanResult<PlexMovie>> maybeMovie = await _movieRepository Either<BaseError, MediaItemScanResult<PlexMovie>> maybeMovie = await _movieRepository
.GetOrAdd(library, incoming) .GetOrAdd(library, incoming)
.BindT(existing => UpdateStatistics(existing, incoming, library, connection, token)) .BindT(existing => UpdateStatistics(pathReplacements, existing, incoming, ffprobePath))
.BindT(existing => UpdateMetadata(existing, incoming, library, connection, token)) .BindT(existing => UpdateMetadata(existing, incoming, library, connection, token))
.BindT(existing => UpdateArtwork(existing, incoming)); .BindT(existing => UpdateArtwork(existing, incoming));
@ -144,11 +147,10 @@ namespace ErsatzTV.Core.Plex
} }
private async Task<Either<BaseError, MediaItemScanResult<PlexMovie>>> UpdateStatistics( private async Task<Either<BaseError, MediaItemScanResult<PlexMovie>>> UpdateStatistics(
List<PlexPathReplacement> pathReplacements,
MediaItemScanResult<PlexMovie> result, MediaItemScanResult<PlexMovie> result,
PlexMovie incoming, PlexMovie incoming,
PlexLibrary library, string ffprobePath)
PlexConnection connection,
PlexServerAuthToken token)
{ {
PlexMovie existing = result.Item; PlexMovie existing = result.Item;
MediaVersion existingVersion = existing.MediaVersions.Head(); MediaVersion existingVersion = existing.MediaVersions.Head();
@ -156,30 +158,37 @@ namespace ErsatzTV.Core.Plex
if (incomingVersion.DateUpdated > existingVersion.DateUpdated || !existingVersion.Streams.Any()) if (incomingVersion.DateUpdated > existingVersion.DateUpdated || !existingVersion.Streams.Any())
{ {
Either<BaseError, Tuple<MovieMetadata, MediaVersion>> maybeStatistics = string localPath = _plexPathReplacementService.GetReplacementPlexPath(
await _plexServerApiClient.GetMovieMetadataAndStatistics( pathReplacements,
library, incoming.MediaVersions.Head().MediaFiles.Head().Path,
incoming.Key.Split("/").Last(), false);
connection,
token); _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath);
Either<BaseError, bool> refreshResult =
await maybeStatistics.Match( await _localStatisticsProvider.RefreshStatistics(ffprobePath, incoming, localPath);
async tuple =>
await refreshResult.Match(
async _ =>
{ {
(MovieMetadata _, MediaVersion mediaVersion) = tuple; foreach (MediaItem updated in await _searchRepository.GetItemToIndex(incoming.Id))
{
_logger.LogDebug( await _searchIndex.UpdateItems(
"Refreshing {Attribute} from {Path}", _searchRepository,
"Plex Statistics", new List<MediaItem> { updated });
existingVersion.MediaFiles.Head().Path); }
existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio;
existingVersion.VideoScanKind = mediaVersion.VideoScanKind;
existingVersion.DateUpdated = mediaVersion.DateUpdated;
await _metadataRepository.UpdatePlexStatistics(existingVersion.Id, mediaVersion); await _metadataRepository.UpdatePlexStatistics(existingVersion.Id, incomingVersion);
}, },
_ => Task.CompletedTask); error =>
{
_logger.LogWarning(
"Unable to refresh {Attribute} for media item {Path}. Error: {Error}",
"Statistics",
localPath,
error.Value);
return Task.CompletedTask;
});
} }
return result; return result;

126
ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs

@ -20,6 +20,7 @@ namespace ErsatzTV.Core.Plex
public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionLibraryScanner public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionLibraryScanner
{ {
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILogger<PlexTelevisionLibraryScanner> _logger; private readonly ILogger<PlexTelevisionLibraryScanner> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMediator _mediator; private readonly IMediator _mediator;
@ -40,6 +41,7 @@ namespace ErsatzTV.Core.Plex
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
IPlexPathReplacementService plexPathReplacementService, IPlexPathReplacementService plexPathReplacementService,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILogger<PlexTelevisionLibraryScanner> logger) ILogger<PlexTelevisionLibraryScanner> logger)
: base(metadataRepository, logger) : base(metadataRepository, logger)
{ {
@ -52,13 +54,15 @@ namespace ErsatzTV.Core.Plex
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
_plexPathReplacementService = plexPathReplacementService; _plexPathReplacementService = plexPathReplacementService;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_localStatisticsProvider = localStatisticsProvider;
_logger = logger; _logger = logger;
} }
public async Task<Either<BaseError, Unit>> ScanLibrary( public async Task<Either<BaseError, Unit>> ScanLibrary(
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token, PlexServerAuthToken token,
PlexLibrary library) PlexLibrary library,
string ffprobePath)
{ {
List<PlexPathReplacement> pathReplacements = await _mediaSourceRepository List<PlexPathReplacement> pathReplacements = await _mediaSourceRepository
.GetPlexPathReplacements(library.MediaSourceId); .GetPlexPathReplacements(library.MediaSourceId);
@ -85,7 +89,7 @@ namespace ErsatzTV.Core.Plex
await maybeShow.Match( await maybeShow.Match(
async result => async result =>
{ {
await ScanSeasons(library, pathReplacements, result.Item, connection, token); await ScanSeasons(library, pathReplacements, result.Item, connection, token, ffprobePath);
if (result.IsAdded) if (result.IsAdded)
{ {
@ -288,7 +292,8 @@ namespace ErsatzTV.Core.Plex
List<PlexPathReplacement> pathReplacements, List<PlexPathReplacement> pathReplacements,
PlexShow show, PlexShow show,
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token) PlexServerAuthToken token,
string ffprobePath)
{ {
Either<BaseError, List<PlexSeason>> entries = await _plexServerApiClient.GetShowSeasons( Either<BaseError, List<PlexSeason>> entries = await _plexServerApiClient.GetShowSeasons(
library, library,
@ -309,7 +314,13 @@ namespace ErsatzTV.Core.Plex
.BindT(existing => UpdateMetadataAndArtwork(existing, incoming)); .BindT(existing => UpdateMetadataAndArtwork(existing, incoming));
await maybeSeason.Match( await maybeSeason.Match(
async season => await ScanEpisodes(library, pathReplacements, season, connection, token), async season => await ScanEpisodes(
library,
pathReplacements,
season,
connection,
token,
ffprobePath),
error => error =>
{ {
_logger.LogWarning( _logger.LogWarning(
@ -373,7 +384,8 @@ namespace ErsatzTV.Core.Plex
List<PlexPathReplacement> pathReplacements, List<PlexPathReplacement> pathReplacements,
PlexSeason season, PlexSeason season,
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token) PlexServerAuthToken token,
string ffprobePath)
{ {
Either<BaseError, List<PlexEpisode>> entries = await _plexServerApiClient.GetSeasonEpisodes( Either<BaseError, List<PlexEpisode>> entries = await _plexServerApiClient.GetSeasonEpisodes(
library, library,
@ -414,11 +426,13 @@ namespace ErsatzTV.Core.Plex
.BindT(existing => UpdateMetadata(existing, incoming)) .BindT(existing => UpdateMetadata(existing, incoming))
.BindT( .BindT(
existing => UpdateStatistics( existing => UpdateStatistics(
pathReplacements,
existing, existing,
incoming, incoming,
library, library,
connection, connection,
token)) token,
ffprobePath))
.BindT(existing => UpdateArtwork(existing, incoming)); .BindT(existing => UpdateArtwork(existing, incoming));
await maybeEpisode.Match( await maybeEpisode.Match(
@ -484,57 +498,89 @@ namespace ErsatzTV.Core.Plex
} }
private async Task<Either<BaseError, PlexEpisode>> UpdateStatistics( private async Task<Either<BaseError, PlexEpisode>> UpdateStatistics(
List<PlexPathReplacement> pathReplacements,
PlexEpisode existing, PlexEpisode existing,
PlexEpisode incoming, PlexEpisode incoming,
PlexLibrary library, PlexLibrary library,
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token) PlexServerAuthToken token,
string ffprobePath)
{ {
MediaVersion existingVersion = existing.MediaVersions.Head(); MediaVersion existingVersion = existing.MediaVersions.Head();
MediaVersion incomingVersion = incoming.MediaVersions.Head(); MediaVersion incomingVersion = incoming.MediaVersions.Head();
if (incomingVersion.DateUpdated > existingVersion.DateUpdated || !existingVersion.Streams.Any()) if (incomingVersion.DateUpdated > existingVersion.DateUpdated || !existingVersion.Streams.Any())
{ {
Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>> maybeStatistics = string localPath = _plexPathReplacementService.GetReplacementPlexPath(
await _plexServerApiClient.GetEpisodeMetadataAndStatistics( pathReplacements,
library, incoming.MediaVersions.Head().MediaFiles.Head().Path,
incoming.Key.Split("/").Last(), false);
connection,
token); _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath);
Either<BaseError, bool> refreshResult =
await maybeStatistics.Match( await _localStatisticsProvider.RefreshStatistics(ffprobePath, incoming, localPath);
async tuple =>
await refreshResult.Match(
async _ =>
{ {
(EpisodeMetadata incomingMetadata, MediaVersion mediaVersion) = tuple; foreach (MediaItem updated in await _searchRepository.GetItemToIndex(incoming.Id))
Option<EpisodeMetadata> maybeExisting = existing.EpisodeMetadata
.Find(em => em.EpisodeNumber == incomingMetadata.EpisodeNumber);
foreach (EpisodeMetadata existingMetadata in maybeExisting)
{ {
foreach (MetadataGuid guid in existingMetadata.Guids await _searchIndex.UpdateItems(
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) _searchRepository,
.ToList()) new List<MediaItem> { updated });
{ }
existingMetadata.Guids.Remove(guid);
await _metadataRepository.RemoveGuid(guid);
}
foreach (MetadataGuid guid in incomingMetadata.Guids Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>> maybeStatistics =
.Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) await _plexServerApiClient.GetEpisodeMetadataAndStatistics(
.ToList()) library,
incoming.Key.Split("/").Last(),
connection,
token);
await maybeStatistics.Match(
async tuple =>
{ {
existingMetadata.Guids.Add(guid); (EpisodeMetadata incomingMetadata, MediaVersion mediaVersion) = tuple;
await _metadataRepository.AddGuid(existingMetadata, guid);
}
}
existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio; Option<EpisodeMetadata> maybeExisting = existing.EpisodeMetadata
existingVersion.VideoScanKind = mediaVersion.VideoScanKind; .Find(em => em.EpisodeNumber == incomingMetadata.EpisodeNumber);
existingVersion.DateUpdated = mediaVersion.DateUpdated; foreach (EpisodeMetadata existingMetadata in maybeExisting)
{
foreach (MetadataGuid guid in existingMetadata.Guids
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
existingMetadata.Guids.Remove(guid);
await _metadataRepository.RemoveGuid(guid);
}
foreach (MetadataGuid guid in incomingMetadata.Guids
.Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
existingMetadata.Guids.Add(guid);
await _metadataRepository.AddGuid(existingMetadata, guid);
}
}
existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio;
existingVersion.VideoScanKind = mediaVersion.VideoScanKind;
existingVersion.DateUpdated = mediaVersion.DateUpdated;
await _metadataRepository.UpdatePlexStatistics(existingVersion.Id, mediaVersion); await _metadataRepository.UpdatePlexStatistics(existingVersion.Id, mediaVersion);
},
_ => Task.CompletedTask);
}, },
_ => Task.CompletedTask); error =>
{
_logger.LogWarning(
"Unable to refresh {Attribute} for media item {Path}. Error: {Error}",
"Statistics",
localPath,
error.Value);
return Task.CompletedTask;
});
} }
return Right<BaseError, PlexEpisode>(existing); return Right<BaseError, PlexEpisode>(existing);

12
ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs

@ -166,25 +166,17 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
() => Task.FromResult(false)); () => Task.FromResult(false));
} }
public async Task<bool> UpdatePlexStatistics(int mediaVersionId, MediaVersion incoming) public Task<bool> UpdatePlexStatistics(int mediaVersionId, MediaVersion incoming) =>
{ _dbConnection.ExecuteAsync(
bool updatedVersion = await _dbConnection.ExecuteAsync(
@"UPDATE MediaVersion SET @"UPDATE MediaVersion SET
SampleAspectRatio = @SampleAspectRatio,
VideoScanKind = @VideoScanKind,
DateUpdated = @DateUpdated DateUpdated = @DateUpdated
WHERE Id = @MediaVersionId", WHERE Id = @MediaVersionId",
new new
{ {
incoming.SampleAspectRatio,
incoming.VideoScanKind,
incoming.DateUpdated, incoming.DateUpdated,
MediaVersionId = mediaVersionId MediaVersionId = mediaVersionId
}).Map(result => result > 0); }).Map(result => result > 0);
return await UpdateLocalStatistics(mediaVersionId, incoming, false) || updatedVersion;
}
public Task<Unit> UpdateArtworkPath(Artwork artwork) => public Task<Unit> UpdateArtworkPath(Artwork artwork) =>
_dbConnection.ExecuteAsync( _dbConnection.ExecuteAsync(
"UPDATE Artwork SET Path = @Path, DateUpdated = @DateUpdated WHERE Id = @Id", "UPDATE Artwork SET Path = @Path, DateUpdated = @DateUpdated WHERE Id = @Id",

2976
ErsatzTV.Infrastructure/Migrations/20210619191334_Add_MediaStreamPixelFormat.Designer.cs generated

File diff suppressed because it is too large Load Diff

34
ErsatzTV.Infrastructure/Migrations/20210619191334_Add_MediaStreamPixelFormat.cs

@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_MediaStreamPixelFormat : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "BitsPerRawSample",
table: "MediaStream",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "PixelFormat",
table: "MediaStream",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BitsPerRawSample",
table: "MediaStream");
migrationBuilder.DropColumn(
name: "PixelFormat",
table: "MediaStream");
}
}
}

2976
ErsatzTV.Infrastructure/Migrations/20210620012739_Reset_AllStatistics.Designer.cs generated

File diff suppressed because it is too large Load Diff

27
ErsatzTV.Infrastructure/Migrations/20210620012739_Reset_AllStatistics.cs

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Reset_AllStatistics : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("UPDATE MediaVersion SET DateUpdated = '0001-01-01 00:00:00'");
migrationBuilder.Sql("UPDATE LibraryFolder SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbyMovie SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbyShow SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbySeason SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbyEpisode SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinMovie SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinShow SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinSeason SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinEpisode SET Etag = NULL");
migrationBuilder.Sql("UPDATE LibraryPath SET LastScan = '0001-01-01 00:00:00'");
migrationBuilder.Sql("UPDATE Library SET LastScan = '0001-01-01 00:00:00'");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

6
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -733,6 +733,9 @@ namespace ErsatzTV.Infrastructure.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("BitsPerRawSample")
.HasColumnType("INTEGER");
b.Property<int>("Channels") b.Property<int>("Channels")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -757,6 +760,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int>("MediaVersionId") b.Property<int>("MediaVersionId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("PixelFormat")
.HasColumnType("TEXT");
b.Property<string>("Profile") b.Property<string>("Profile")
.HasColumnType("TEXT"); .HasColumnType("TEXT");

Loading…
Cancel
Save