ffmpeg and ffprobe validation fixes (#63)

* abort building playout if any collection contains a zero-duration item

* surface errors calling ffprobe

* improve ffmpeg/ffprobe path validation
This commit is contained in:
Jason Dove
2021-03-12 02:20:18 +00:00
committed by GitHub
parent c240169fc9
commit 1587ac7d62
9 changed files with 141 additions and 21 deletions
@@ -1,10 +1,11 @@
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using LanguageExt;
namespace ErsatzTV.Core.Interfaces.Metadata
{
public interface ILocalStatisticsProvider
{
Task<bool> RefreshStatistics(string ffprobePath, MediaItem mediaItem);
Task<Either<BaseError, Unit>> RefreshStatistics(string ffprobePath, MediaItem mediaItem);
}
}
+9 -1
View File
@@ -80,7 +80,15 @@ namespace ErsatzTV.Core.Metadata
if (version.DateUpdated < _localFileSystem.GetLastWriteTime(path))
{
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", path);
await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);
Either<BaseError, Unit> refreshResult =
await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);
refreshResult.IfLeft(
error =>
_logger.LogWarning(
"Unable to refresh {Attribute} for media item {Path}. Error: {Error}",
"Statistics",
path,
error.Value));
}
return mediaItem;
@@ -29,7 +29,7 @@ namespace ErsatzTV.Core.Metadata
_logger = logger;
}
public async Task<bool> RefreshStatistics(string ffprobePath, MediaItem mediaItem)
public async Task<Either<BaseError, Unit>> RefreshStatistics(string ffprobePath, MediaItem mediaItem)
{
try
{
@@ -40,14 +40,20 @@ namespace ErsatzTV.Core.Metadata
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
};
FFprobe ffprobe = await GetProbeOutput(ffprobePath, filePath);
MediaVersion version = ProjectToMediaVersion(ffprobe);
return await ApplyVersionUpdate(mediaItem, version, filePath);
Either<BaseError, FFprobe> maybeProbe = await GetProbeOutput(ffprobePath, filePath);
return await maybeProbe.Match(
async ffprobe =>
{
MediaVersion version = ProjectToMediaVersion(ffprobe);
await ApplyVersionUpdate(mediaItem, version, filePath);
return Right<BaseError, Unit>(Unit.Default);
},
error => Task.FromResult(Left<BaseError, Unit>(error)));
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to refresh statistics for media item {Id}", mediaItem.Id);
return false;
return BaseError.New(ex.Message);
}
}
@@ -76,7 +82,7 @@ namespace ErsatzTV.Core.Metadata
return await _mediaItemRepository.Update(mediaItem) && durationChange;
}
private Task<FFprobe> GetProbeOutput(string ffprobePath, string filePath)
private Task<Either<BaseError, FFprobe>> GetProbeOutput(string ffprobePath, string filePath)
{
var startInfo = new ProcessStartInfo
{
@@ -101,11 +107,13 @@ namespace ErsatzTV.Core.Metadata
};
probe.Start();
return probe.StandardOutput.ReadToEndAsync().MapAsync(
return probe.StandardOutput.ReadToEndAsync().MapAsync<string, Either<BaseError, FFprobe>>(
async output =>
{
await probe.WaitForExitAsync();
return JsonConvert.DeserializeObject<FFprobe>(output);
return probe.ExitCode == 0
? JsonConvert.DeserializeObject<FFprobe>(output)
: BaseError.New($"FFprobe at {ffprobePath} exited with code {probe.ExitCode}");
});
}
@@ -88,6 +88,25 @@ namespace ErsatzTV.Core.Scheduling
return playout;
}
Option<CollectionKey> zeroDurationCollection = collectionMediaItems.Find(
c => c.Value.Any(
mi => mi switch
{
Movie m => m.MediaVersions.HeadOrNone().Map(mv => mv.Duration).IfNone(TimeSpan.Zero) ==
TimeSpan.Zero,
Episode e => e.MediaVersions.HeadOrNone().Map(mv => mv.Duration).IfNone(TimeSpan.Zero) ==
TimeSpan.Zero,
_ => true
})).Map(c => c.Key);
if (zeroDurationCollection.IsSome)
{
_logger.LogError(
"Unable to rebuild playout; collection {@CollectionKey} contains items with zero duration!",
zeroDurationCollection.ValueUnsafe());
return playout;
}
playout.Items ??= new List<PlayoutItem>();
playout.ProgramScheduleAnchors ??= new List<PlayoutProgramScheduleAnchor>();