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
This commit is contained in:
@@ -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/).
|
||||
|
||||
## [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
|
||||
### Added
|
||||
|
||||
@@ -68,7 +68,8 @@ namespace ErsatzTV.Application.Emby.Commands
|
||||
private async Task<Unit> Synchronize(RequestParameters parameters)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -104,12 +105,14 @@ namespace ErsatzTV.Application.Emby.Commands
|
||||
|
||||
private async Task<Validation<BaseError, RequestParameters>> Validate(
|
||||
ISynchronizeEmbyLibraryById request) =>
|
||||
(await ValidateConnection(request), await EmbyLibraryMustExist(request), await ValidateFFprobePath())
|
||||
(await ValidateConnection(request), await EmbyLibraryMustExist(request),
|
||||
await ValidateLibraryRefreshInterval(), await ValidateFFprobePath())
|
||||
.Apply(
|
||||
(connectionParameters, embyLibrary, ffprobePath) => new RequestParameters(
|
||||
(connectionParameters, embyLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters(
|
||||
connectionParameters,
|
||||
embyLibrary,
|
||||
request.ForceScan,
|
||||
libraryRefreshInterval,
|
||||
ffprobePath
|
||||
));
|
||||
|
||||
@@ -149,6 +152,11 @@ namespace ErsatzTV.Application.Emby.Commands
|
||||
_mediaSourceRepository.GetEmbyLibrary(request.EmbyLibraryId)
|
||||
.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() =>
|
||||
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
|
||||
.FilterT(File.Exists)
|
||||
@@ -160,6 +168,7 @@ namespace ErsatzTV.Application.Emby.Commands
|
||||
ConnectionParameters ConnectionParameters,
|
||||
EmbyLibrary Library,
|
||||
bool ForceScan,
|
||||
int LibraryRefreshInterval,
|
||||
string FFprobePath);
|
||||
|
||||
private record ConnectionParameters(
|
||||
|
||||
@@ -68,7 +68,8 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
||||
private async Task<Unit> Synchronize(RequestParameters parameters)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -104,12 +105,14 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
||||
|
||||
private async Task<Validation<BaseError, RequestParameters>> Validate(
|
||||
ISynchronizeJellyfinLibraryById request) =>
|
||||
(await ValidateConnection(request), await JellyfinLibraryMustExist(request), await ValidateFFprobePath())
|
||||
(await ValidateConnection(request), await JellyfinLibraryMustExist(request),
|
||||
await ValidateLibraryRefreshInterval(), await ValidateFFprobePath())
|
||||
.Apply(
|
||||
(connectionParameters, jellyfinLibrary, ffprobePath) => new RequestParameters(
|
||||
(connectionParameters, jellyfinLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters(
|
||||
connectionParameters,
|
||||
jellyfinLibrary,
|
||||
request.ForceScan,
|
||||
libraryRefreshInterval,
|
||||
ffprobePath
|
||||
));
|
||||
|
||||
@@ -149,6 +152,11 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
||||
_mediaSourceRepository.GetJellyfinLibrary(request.JellyfinLibraryId)
|
||||
.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() =>
|
||||
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
|
||||
.FilterT(File.Exists)
|
||||
@@ -160,6 +168,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
||||
ConnectionParameters ConnectionParameters,
|
||||
JellyfinLibrary Library,
|
||||
bool ForceScan,
|
||||
int LibraryRefreshInterval,
|
||||
string FFprobePath);
|
||||
|
||||
private record ConnectionParameters(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -74,13 +75,15 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
await _plexMovieLibraryScanner.ScanLibrary(
|
||||
parameters.ConnectionParameters.ActiveConnection,
|
||||
parameters.ConnectionParameters.PlexServerAuthToken,
|
||||
parameters.Library);
|
||||
parameters.Library,
|
||||
parameters.FFprobePath);
|
||||
break;
|
||||
case LibraryMediaKind.Shows:
|
||||
await _plexTelevisionLibraryScanner.ScanLibrary(
|
||||
parameters.ConnectionParameters.ActiveConnection,
|
||||
parameters.ConnectionParameters.PlexServerAuthToken,
|
||||
parameters.Library);
|
||||
parameters.Library,
|
||||
parameters.FFprobePath);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -100,13 +103,14 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
|
||||
private async Task<Validation<BaseError, RequestParameters>> Validate(ISynchronizePlexLibraryById request) =>
|
||||
(await ValidateConnection(request), await PlexLibraryMustExist(request),
|
||||
await ValidateLibraryRefreshInterval())
|
||||
await ValidateLibraryRefreshInterval(), await ValidateFFprobePath())
|
||||
.Apply(
|
||||
(connectionParameters, plexLibrary, libraryRefreshInterval) => new RequestParameters(
|
||||
(connectionParameters, plexLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters(
|
||||
connectionParameters,
|
||||
plexLibrary,
|
||||
request.ForceScan,
|
||||
libraryRefreshInterval
|
||||
libraryRefreshInterval,
|
||||
ffprobePath
|
||||
));
|
||||
|
||||
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
|
||||
@@ -150,11 +154,19 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
.FilterT(lri => lri > 0)
|
||||
.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(
|
||||
ConnectionParameters ConnectionParameters,
|
||||
PlexLibrary Library,
|
||||
bool ForceScan,
|
||||
int LibraryRefreshInterval);
|
||||
int LibraryRefreshInterval,
|
||||
string FFprobePath);
|
||||
|
||||
private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
public string Title { get; set; }
|
||||
public bool Default { get; set; }
|
||||
public bool Forced { get; set; }
|
||||
public string PixelFormat { get; set; }
|
||||
public int BitsPerRawSample { get; set; }
|
||||
public int MediaVersionId { get; set; }
|
||||
public MediaVersion MediaVersion { get; set; }
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
private IDisplaySize _resolution;
|
||||
private Option<IDisplaySize> _scaleToSize = None;
|
||||
private Option<ChannelWatermark> _watermark;
|
||||
private string _pixelFormat;
|
||||
|
||||
public FFmpegComplexFilterBuilder WithHardwareAcceleration(HardwareAccelerationKind hardwareAccelerationKind)
|
||||
{
|
||||
@@ -63,6 +64,12 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegComplexFilterBuilder WithInputPixelFormat(string pixelFormat)
|
||||
{
|
||||
_pixelFormat = pixelFormat;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegComplexFilterBuilder WithWatermark(Option<ChannelWatermark> watermark, IDisplaySize resolution)
|
||||
{
|
||||
_watermark = watermark;
|
||||
@@ -128,6 +135,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
string filter = acceleration switch
|
||||
{
|
||||
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.Vaapi => $"scale_vaapi=w={size.Width}:h={size.Height}",
|
||||
_ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear"
|
||||
@@ -150,6 +159,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
string format = acceleration switch
|
||||
{
|
||||
HardwareAccelerationKind.Vaapi => "format=nv12|vaapi",
|
||||
HardwareAccelerationKind.Nvenc when _scaleToSize.IsNone && _pixelFormat == "yuv420p10le" =>
|
||||
"format=p010le,format=nv12",
|
||||
_ => "format=nv12"
|
||||
};
|
||||
videoFilterQueue.Add(format);
|
||||
|
||||
@@ -175,7 +175,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
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))
|
||||
{
|
||||
@@ -183,7 +183,9 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
_arguments.Add(qsvCodec);
|
||||
}
|
||||
|
||||
_complexFilterBuilder = _complexFilterBuilder.WithInputCodec(codec);
|
||||
_complexFilterBuilder = _complexFilterBuilder
|
||||
.WithInputCodec(codec)
|
||||
.WithInputPixelFormat(pixelFormat);
|
||||
|
||||
_arguments.Add("-i");
|
||||
_arguments.Add($"{input}");
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
|
||||
.WithSeek(playbackSettings.StreamSeek)
|
||||
.WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec)
|
||||
.WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec, videoStream.PixelFormat)
|
||||
.WithWatermark(maybeWatermark, maybeWatermarkPath, channel.FFmpegProfile.Resolution, isAnimated)
|
||||
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)
|
||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace ErsatzTV.Core.Interfaces.Plex
|
||||
Task<Either<BaseError, Unit>> ScanLibrary(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token,
|
||||
PlexLibrary library);
|
||||
PlexLibrary library,
|
||||
string ffprobePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace ErsatzTV.Core.Interfaces.Plex
|
||||
Task<Either<BaseError, Unit>> ScanLibrary(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token,
|
||||
PlexLibrary library);
|
||||
PlexLibrary library,
|
||||
string ffprobePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,9 +194,15 @@ namespace ErsatzTV.Core.Metadata
|
||||
MediaStreamKind = MediaStreamKind.Video,
|
||||
Index = videoStream.index,
|
||||
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)
|
||||
{
|
||||
stream.Default = videoStream.disposition.@default == 1;
|
||||
@@ -262,8 +268,10 @@ namespace ErsatzTV.Core.Metadata
|
||||
int height,
|
||||
string sample_aspect_ratio,
|
||||
string display_aspect_ratio,
|
||||
string pix_fmt,
|
||||
string field_order,
|
||||
string r_frame_rate,
|
||||
string bits_per_raw_sample,
|
||||
FFprobeDisposition disposition,
|
||||
FFProbeTags tags);
|
||||
// ReSharper restore InconsistentNaming
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
@@ -18,6 +17,7 @@ namespace ErsatzTV.Core.Plex
|
||||
public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScanner
|
||||
{
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
||||
private readonly ILogger<PlexMovieLibraryScanner> _logger;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IMediator _mediator;
|
||||
@@ -38,6 +38,7 @@ namespace ErsatzTV.Core.Plex
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IPlexPathReplacementService plexPathReplacementService,
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILogger<PlexMovieLibraryScanner> logger)
|
||||
: base(metadataRepository, logger)
|
||||
{
|
||||
@@ -50,13 +51,15 @@ namespace ErsatzTV.Core.Plex
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_plexPathReplacementService = plexPathReplacementService;
|
||||
_localFileSystem = localFileSystem;
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> ScanLibrary(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token,
|
||||
PlexLibrary library)
|
||||
PlexLibrary library,
|
||||
string ffprobePath)
|
||||
{
|
||||
List<PlexPathReplacement> pathReplacements = await _mediaSourceRepository
|
||||
.GetPlexPathReplacements(library.MediaSourceId);
|
||||
@@ -95,7 +98,7 @@ namespace ErsatzTV.Core.Plex
|
||||
// TODO: figure out how to rebuild playlists
|
||||
Either<BaseError, MediaItemScanResult<PlexMovie>> maybeMovie = await _movieRepository
|
||||
.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 => UpdateArtwork(existing, incoming));
|
||||
|
||||
@@ -144,11 +147,10 @@ namespace ErsatzTV.Core.Plex
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<PlexMovie>>> UpdateStatistics(
|
||||
List<PlexPathReplacement> pathReplacements,
|
||||
MediaItemScanResult<PlexMovie> result,
|
||||
PlexMovie incoming,
|
||||
PlexLibrary library,
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token)
|
||||
string ffprobePath)
|
||||
{
|
||||
PlexMovie existing = result.Item;
|
||||
MediaVersion existingVersion = existing.MediaVersions.Head();
|
||||
@@ -156,30 +158,37 @@ namespace ErsatzTV.Core.Plex
|
||||
|
||||
if (incomingVersion.DateUpdated > existingVersion.DateUpdated || !existingVersion.Streams.Any())
|
||||
{
|
||||
Either<BaseError, Tuple<MovieMetadata, MediaVersion>> maybeStatistics =
|
||||
await _plexServerApiClient.GetMovieMetadataAndStatistics(
|
||||
library,
|
||||
incoming.Key.Split("/").Last(),
|
||||
connection,
|
||||
token);
|
||||
string localPath = _plexPathReplacementService.GetReplacementPlexPath(
|
||||
pathReplacements,
|
||||
incoming.MediaVersions.Head().MediaFiles.Head().Path,
|
||||
false);
|
||||
|
||||
await maybeStatistics.Match(
|
||||
async tuple =>
|
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath);
|
||||
Either<BaseError, bool> refreshResult =
|
||||
await _localStatisticsProvider.RefreshStatistics(ffprobePath, incoming, localPath);
|
||||
|
||||
await refreshResult.Match(
|
||||
async _ =>
|
||||
{
|
||||
(MovieMetadata _, MediaVersion mediaVersion) = tuple;
|
||||
foreach (MediaItem updated in await _searchRepository.GetItemToIndex(incoming.Id))
|
||||
{
|
||||
await _searchIndex.UpdateItems(
|
||||
_searchRepository,
|
||||
new List<MediaItem> { updated });
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Refreshing {Attribute} from {Path}",
|
||||
"Plex Statistics",
|
||||
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;
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace ErsatzTV.Core.Plex
|
||||
public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionLibraryScanner
|
||||
{
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
||||
private readonly ILogger<PlexTelevisionLibraryScanner> _logger;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IMediator _mediator;
|
||||
@@ -40,6 +41,7 @@ namespace ErsatzTV.Core.Plex
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IPlexPathReplacementService plexPathReplacementService,
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILogger<PlexTelevisionLibraryScanner> logger)
|
||||
: base(metadataRepository, logger)
|
||||
{
|
||||
@@ -52,13 +54,15 @@ namespace ErsatzTV.Core.Plex
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_plexPathReplacementService = plexPathReplacementService;
|
||||
_localFileSystem = localFileSystem;
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> ScanLibrary(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token,
|
||||
PlexLibrary library)
|
||||
PlexLibrary library,
|
||||
string ffprobePath)
|
||||
{
|
||||
List<PlexPathReplacement> pathReplacements = await _mediaSourceRepository
|
||||
.GetPlexPathReplacements(library.MediaSourceId);
|
||||
@@ -85,7 +89,7 @@ namespace ErsatzTV.Core.Plex
|
||||
await maybeShow.Match(
|
||||
async result =>
|
||||
{
|
||||
await ScanSeasons(library, pathReplacements, result.Item, connection, token);
|
||||
await ScanSeasons(library, pathReplacements, result.Item, connection, token, ffprobePath);
|
||||
|
||||
if (result.IsAdded)
|
||||
{
|
||||
@@ -288,7 +292,8 @@ namespace ErsatzTV.Core.Plex
|
||||
List<PlexPathReplacement> pathReplacements,
|
||||
PlexShow show,
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token)
|
||||
PlexServerAuthToken token,
|
||||
string ffprobePath)
|
||||
{
|
||||
Either<BaseError, List<PlexSeason>> entries = await _plexServerApiClient.GetShowSeasons(
|
||||
library,
|
||||
@@ -309,7 +314,13 @@ namespace ErsatzTV.Core.Plex
|
||||
.BindT(existing => UpdateMetadataAndArtwork(existing, incoming));
|
||||
|
||||
await maybeSeason.Match(
|
||||
async season => await ScanEpisodes(library, pathReplacements, season, connection, token),
|
||||
async season => await ScanEpisodes(
|
||||
library,
|
||||
pathReplacements,
|
||||
season,
|
||||
connection,
|
||||
token,
|
||||
ffprobePath),
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
@@ -373,7 +384,8 @@ namespace ErsatzTV.Core.Plex
|
||||
List<PlexPathReplacement> pathReplacements,
|
||||
PlexSeason season,
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token)
|
||||
PlexServerAuthToken token,
|
||||
string ffprobePath)
|
||||
{
|
||||
Either<BaseError, List<PlexEpisode>> entries = await _plexServerApiClient.GetSeasonEpisodes(
|
||||
library,
|
||||
@@ -414,11 +426,13 @@ namespace ErsatzTV.Core.Plex
|
||||
.BindT(existing => UpdateMetadata(existing, incoming))
|
||||
.BindT(
|
||||
existing => UpdateStatistics(
|
||||
pathReplacements,
|
||||
existing,
|
||||
incoming,
|
||||
library,
|
||||
connection,
|
||||
token))
|
||||
token,
|
||||
ffprobePath))
|
||||
.BindT(existing => UpdateArtwork(existing, incoming));
|
||||
|
||||
await maybeEpisode.Match(
|
||||
@@ -484,17 +498,38 @@ namespace ErsatzTV.Core.Plex
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, PlexEpisode>> UpdateStatistics(
|
||||
List<PlexPathReplacement> pathReplacements,
|
||||
PlexEpisode existing,
|
||||
PlexEpisode incoming,
|
||||
PlexLibrary library,
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token)
|
||||
PlexServerAuthToken token,
|
||||
string ffprobePath)
|
||||
{
|
||||
MediaVersion existingVersion = existing.MediaVersions.Head();
|
||||
MediaVersion incomingVersion = incoming.MediaVersions.Head();
|
||||
|
||||
if (incomingVersion.DateUpdated > existingVersion.DateUpdated || !existingVersion.Streams.Any())
|
||||
{
|
||||
string localPath = _plexPathReplacementService.GetReplacementPlexPath(
|
||||
pathReplacements,
|
||||
incoming.MediaVersions.Head().MediaFiles.Head().Path,
|
||||
false);
|
||||
|
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath);
|
||||
Either<BaseError, bool> refreshResult =
|
||||
await _localStatisticsProvider.RefreshStatistics(ffprobePath, incoming, localPath);
|
||||
|
||||
await refreshResult.Match(
|
||||
async _ =>
|
||||
{
|
||||
foreach (MediaItem updated in await _searchRepository.GetItemToIndex(incoming.Id))
|
||||
{
|
||||
await _searchIndex.UpdateItems(
|
||||
_searchRepository,
|
||||
new List<MediaItem> { updated });
|
||||
}
|
||||
|
||||
Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>> maybeStatistics =
|
||||
await _plexServerApiClient.GetEpisodeMetadataAndStatistics(
|
||||
library,
|
||||
@@ -535,6 +570,17 @@ namespace ErsatzTV.Core.Plex
|
||||
await _metadataRepository.UpdatePlexStatistics(existingVersion.Id, mediaVersion);
|
||||
},
|
||||
_ => 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);
|
||||
|
||||
@@ -166,25 +166,17 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
() => Task.FromResult(false));
|
||||
}
|
||||
|
||||
public async Task<bool> UpdatePlexStatistics(int mediaVersionId, MediaVersion incoming)
|
||||
{
|
||||
bool updatedVersion = await _dbConnection.ExecuteAsync(
|
||||
public Task<bool> UpdatePlexStatistics(int mediaVersionId, MediaVersion incoming) =>
|
||||
_dbConnection.ExecuteAsync(
|
||||
@"UPDATE MediaVersion SET
|
||||
SampleAspectRatio = @SampleAspectRatio,
|
||||
VideoScanKind = @VideoScanKind,
|
||||
DateUpdated = @DateUpdated
|
||||
WHERE Id = @MediaVersionId",
|
||||
new
|
||||
{
|
||||
incoming.SampleAspectRatio,
|
||||
incoming.VideoScanKind,
|
||||
incoming.DateUpdated,
|
||||
MediaVersionId = mediaVersionId
|
||||
}).Map(result => result > 0);
|
||||
|
||||
return await UpdateLocalStatistics(mediaVersionId, incoming, false) || updatedVersion;
|
||||
}
|
||||
|
||||
public Task<Unit> UpdateArtworkPath(Artwork artwork) =>
|
||||
_dbConnection.ExecuteAsync(
|
||||
"UPDATE Artwork SET Path = @Path, DateUpdated = @DateUpdated WHERE Id = @Id",
|
||||
|
||||
+2976
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -733,6 +733,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BitsPerRawSample")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Channels")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -757,6 +760,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int>("MediaVersionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PixelFormat")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Profile")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user