jellyfin/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs

279 lines
10 KiB
C#
Raw Normal View History

2019-10-18 11:52:32 +00:00
using System;
using System.Diagnostics;
2019-10-18 11:52:32 +00:00
using System.Collections.Concurrent;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Diagnostics;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
using UtfUnknown;
namespace MediaBrowser.MediaEncoding.Attachments
{
public class AttachmentExtractor : IAttachmentExtractor
{
private readonly ILibraryManager _libraryManager;
private readonly ILogger _logger;
private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
private readonly IMediaEncoder _mediaEncoder;
private readonly IMediaSourceManager _mediaSourceManager;
public AttachmentExtractor(
ILibraryManager libraryManager,
2019-11-07 16:55:39 +00:00
ILogger<AttachmentExtractor> logger,
2019-10-18 11:52:32 +00:00
IApplicationPaths appPaths,
IFileSystem fileSystem,
IMediaEncoder mediaEncoder,
IMediaSourceManager mediaSourceManager)
2019-10-18 11:52:32 +00:00
{
_libraryManager = libraryManager;
2019-11-07 16:55:39 +00:00
_logger = logger;
2019-10-18 11:52:32 +00:00
_appPaths = appPaths;
_fileSystem = fileSystem;
_mediaEncoder = mediaEncoder;
_mediaSourceManager = mediaSourceManager;
}
private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
public async Task<(MediaAttachment attachment, Stream stream)> GetAttachment(BaseItem item, string mediaSourceId, int attachmentStreamIndex, CancellationToken cancellationToken)
{
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}
2019-10-18 11:52:32 +00:00
if (string.IsNullOrWhiteSpace(mediaSourceId))
{
throw new ArgumentNullException(nameof(mediaSourceId));
}
var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false);
var mediaSource = mediaSources
.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
if (mediaSource == null)
{
throw new ResourceNotFoundException($"MediaSource {mediaSourceId} not found");
}
var mediaAttachment = mediaSource.MediaAttachments
.FirstOrDefault(i => i.Index == attachmentStreamIndex);
if (mediaAttachment == null)
{
throw new ResourceNotFoundException($"MediaSource {mediaSourceId} has no attachment with stream index {attachmentStreamIndex}");
}
2019-10-18 11:52:32 +00:00
var attachmentStream = await GetAttachmentStream(mediaSource, mediaAttachment, cancellationToken)
.ConfigureAwait(false);
return (mediaAttachment, attachmentStream);
}
private async Task<Stream> GetAttachmentStream(
MediaSourceInfo mediaSource,
MediaAttachment mediaAttachment,
CancellationToken cancellationToken)
{
var inputFiles = new[] { mediaSource.Path };
var attachmentPath = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, mediaAttachment, cancellationToken).ConfigureAwait(false);
var stream = await GetAttachmentStream(attachmentPath, cancellationToken).ConfigureAwait(false);
2019-10-18 11:52:32 +00:00
return stream;
}
private async Task<Stream> GetAttachmentStream(
string path,
CancellationToken cancellationToken)
{
return File.OpenRead(path);
}
private async Task<String> GetReadableFile(
2019-10-18 11:52:32 +00:00
string mediaPath,
string[] inputFiles,
MediaProtocol protocol,
MediaAttachment mediaAttachment,
CancellationToken cancellationToken)
{
var outputPath = GetAttachmentCachePath(mediaPath, protocol, mediaAttachment.Index);
await ExtractAttachment(inputFiles, protocol, mediaAttachment.Index, outputPath, cancellationToken)
.ConfigureAwait(false);
return outputPath;
2019-10-18 11:52:32 +00:00
}
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
new ConcurrentDictionary<string, SemaphoreSlim>();
private SemaphoreSlim GetLock(string filename)
{
return _semaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
}
private async Task ExtractAttachment(
string[] inputFiles,
MediaProtocol protocol,
int attachmentStreamIndex,
string outputPath,
CancellationToken cancellationToken)
{
var semaphore = GetLock(outputPath);
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (!File.Exists(outputPath))
{
await ExtractAttachmentInternal(_mediaEncoder.GetInputArgument(inputFiles, protocol), attachmentStreamIndex, outputPath, cancellationToken).ConfigureAwait(false);
}
}
finally
{
semaphore.Release();
}
}
private async Task ExtractAttachmentInternal(
string inputPath,
int attachmentStreamIndex,
string outputPath,
CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(inputPath))
{
throw new ArgumentNullException(nameof(inputPath));
}
if (string.IsNullOrEmpty(outputPath))
{
throw new ArgumentNullException(nameof(outputPath));
}
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
var processArgs = string.Format(
CultureInfo.InvariantCulture,
"-dump_attachment:{1} {2} -i {0} -t 0 -f null null",
inputPath,
attachmentStreamIndex,
outputPath);
var startInfo = new ProcessStartInfo
2019-10-18 11:52:32 +00:00
{
Arguments = processArgs,
FileName = _mediaEncoder.EncoderPath,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
2019-10-18 11:52:32 +00:00
ErrorDialog = false
};
var process = new Process
{
StartInfo = startInfo
};
2019-10-18 11:52:32 +00:00
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
try
{
process.Start();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting ffmpeg");
throw;
}
var processTcs = new TaskCompletionSource<bool>();
process.EnableRaisingEvents = true;
process.Exited += (sender, args) => processTcs.TrySetResult(true);
var unregister = cancellationToken.Register(() => processTcs.TrySetResult(process.HasExited));
var ranToCompletion = await processTcs.Task.ConfigureAwait(false);
unregister.Dispose();
2019-10-18 11:52:32 +00:00
if (!ranToCompletion)
{
try
{
_logger.LogWarning("Killing ffmpeg attachment extraction process");
process.Kill();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error killing attachment extraction process");
}
}
var exitCode = ranToCompletion ? process.ExitCode : -1;
process.Dispose();
var failed = false;
if (exitCode != 0)
2019-10-18 11:52:32 +00:00
{
failed = true;
2019-11-04 16:16:57 +00:00
_logger.LogWarning("Deleting extracted attachment {Path} due to failure: {ExitCode}", outputPath, exitCode);
2019-10-18 11:52:32 +00:00
try
{
if (File.Exists(outputPath))
{
_fileSystem.DeleteFile(outputPath);
}
2019-10-18 11:52:32 +00:00
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting extracted attachment {Path}", outputPath);
}
}
else if (!File.Exists(outputPath))
{
failed = true;
}
if (failed)
{
var msg = $"ffmpeg attachment extraction failed for {inputPath} to {outputPath}";
_logger.LogError(msg);
throw new Exception(msg);
}
else
{
2019-11-04 16:39:40 +00:00
_logger.LogInformation("ffmpeg attachment extraction completed for {Path} to {Path}", inputPath, outputPath);
2019-10-18 11:52:32 +00:00
}
}
private string GetAttachmentCachePath(string mediaPath, MediaProtocol protocol, int attachmentStreamIndex)
{
string filename;
2019-10-18 11:52:32 +00:00
if (protocol == MediaProtocol.File)
{
var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
2019-11-04 16:45:31 +00:00
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D");
2019-10-18 11:52:32 +00:00
}
else
{
2019-11-04 16:45:31 +00:00
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D");
2019-10-18 11:52:32 +00:00
}
2019-11-04 16:45:31 +00:00
var prefix = filename.Substring(0, 1);
return Path.Combine(AttachmentCachePath, prefix, filename);
2019-10-18 11:52:32 +00:00
}
}
}