aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs')
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs416
1 files changed, 115 insertions, 301 deletions
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 431fc0b17..48a0654bb 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -1,7 +1,4 @@
-#pragma warning disable CS1591
-
using System;
-using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@@ -9,28 +6,27 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Attachments
{
+ /// <inheritdoc cref="IAttachmentExtractor"/>
public sealed class AttachmentExtractor : IAttachmentExtractor, IDisposable
{
private readonly ILogger<AttachmentExtractor> _logger;
- private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
private readonly IMediaEncoder _mediaEncoder;
private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IPathManager _pathManager;
private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
{
@@ -38,18 +34,26 @@ namespace MediaBrowser.MediaEncoding.Attachments
o.PoolInitialFill = 1;
});
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AttachmentExtractor"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger{AttachmentExtractor}"/>.</param>
+ /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
+ /// <param name="mediaEncoder">The <see cref="IMediaEncoder"/>.</param>
+ /// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/>.</param>
+ /// <param name="pathManager">The <see cref="IPathManager"/>.</param>
public AttachmentExtractor(
ILogger<AttachmentExtractor> logger,
- IApplicationPaths appPaths,
IFileSystem fileSystem,
IMediaEncoder mediaEncoder,
- IMediaSourceManager mediaSourceManager)
+ IMediaSourceManager mediaSourceManager,
+ IPathManager pathManager)
{
_logger = logger;
- _appPaths = appPaths;
_fileSystem = fileSystem;
_mediaEncoder = mediaEncoder;
_mediaSourceManager = mediaSourceManager;
+ _pathManager = pathManager;
}
/// <inheritdoc />
@@ -77,350 +81,177 @@ namespace MediaBrowser.MediaEncoding.Attachments
throw new ResourceNotFoundException($"MediaSource {mediaSourceId} has no attachment with stream index {attachmentStreamIndex}");
}
+ if (string.Equals(mediaAttachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ResourceNotFoundException($"Attachment with stream index {attachmentStreamIndex} can't be extracted for MediaSource {mediaSourceId}");
+ }
+
var attachmentStream = await GetAttachmentStream(mediaSource, mediaAttachment, cancellationToken)
.ConfigureAwait(false);
return (mediaAttachment, attachmentStream);
}
+ /// <inheritdoc />
public async Task ExtractAllAttachments(
string inputFile,
MediaSourceInfo mediaSource,
- string outputPath,
CancellationToken cancellationToken)
{
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => !string.IsNullOrEmpty(a.FileName)
&& (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
- if (shouldExtractOneByOne)
- {
- var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
- foreach (var i in attachmentIndexes)
- {
- var newName = Path.Join(outputPath, i.ToString(CultureInfo.InvariantCulture));
- await ExtractAttachment(inputFile, mediaSource, i, newName, cancellationToken).ConfigureAwait(false);
- }
- }
- else
+ if (shouldExtractOneByOne && !inputFile.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
{
- using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
+ foreach (var attachment in mediaSource.MediaAttachments)
{
- if (!Directory.Exists(outputPath))
+ if (!string.Equals(attachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
{
- await ExtractAllAttachmentsInternal(
- _mediaEncoder.GetInputArgument(inputFile, mediaSource),
- outputPath,
- false,
- cancellationToken).ConfigureAwait(false);
+ await ExtractAttachment(inputFile, mediaSource, attachment, cancellationToken).ConfigureAwait(false);
}
}
}
- }
-
- public async Task ExtractAllAttachmentsExternal(
- string inputArgument,
- string id,
- string outputPath,
- CancellationToken cancellationToken)
- {
- using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
+ else
{
- if (!File.Exists(Path.Join(outputPath, id)))
- {
- await ExtractAllAttachmentsInternal(
- inputArgument,
- outputPath,
- true,
- cancellationToken).ConfigureAwait(false);
-
- if (Directory.Exists(outputPath))
- {
- File.Create(Path.Join(outputPath, id));
- }
- }
+ await ExtractAllAttachmentsInternal(
+ inputFile,
+ mediaSource,
+ false,
+ cancellationToken).ConfigureAwait(false);
}
}
private async Task ExtractAllAttachmentsInternal(
- string inputPath,
- string outputPath,
+ string inputFile,
+ MediaSourceInfo mediaSource,
bool isExternal,
CancellationToken cancellationToken)
{
- ArgumentException.ThrowIfNullOrEmpty(inputPath);
- ArgumentException.ThrowIfNullOrEmpty(outputPath);
-
- Directory.CreateDirectory(outputPath);
-
- var processArgs = string.Format(
- CultureInfo.InvariantCulture,
- "-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null",
- inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
- inputPath);
+ var inputPath = _mediaEncoder.GetInputArgument(inputFile, mediaSource);
- int exitCode;
+ ArgumentException.ThrowIfNullOrEmpty(inputPath);
- using (var process = new Process
- {
- StartInfo = new ProcessStartInfo
- {
- Arguments = processArgs,
- FileName = _mediaEncoder.EncoderPath,
- UseShellExecute = false,
- CreateNoWindow = true,
- WindowStyle = ProcessWindowStyle.Hidden,
- WorkingDirectory = outputPath,
- ErrorDialog = false
- },
- EnableRaisingEvents = true
- })
+ var outputFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ using (await _semaphoreLocks.LockAsync(outputFolder, cancellationToken).ConfigureAwait(false))
{
- _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
-
- process.Start();
-
- try
+ var directory = Directory.CreateDirectory(outputFolder);
+ var fileNames = directory.GetFiles("*", SearchOption.TopDirectoryOnly).Select(f => f.Name).ToHashSet();
+ var missingFiles = mediaSource.MediaAttachments.Where(a => a.FileName is not null && !fileNames.Contains(a.FileName) && !string.Equals(a.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase));
+ if (!missingFiles.Any())
{
- await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
- exitCode = process.ExitCode;
- }
- catch (OperationCanceledException)
- {
- process.Kill(true);
- exitCode = -1;
+ // Skip extraction if all files already exist
+ return;
}
- }
- var failed = false;
+ var processArgs = string.Format(
+ CultureInfo.InvariantCulture,
+ "-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null",
+ inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
+ inputPath);
- if (exitCode != 0)
- {
- if (isExternal && exitCode == 1)
- {
- // ffmpeg returns exitCode 1 because there is no video or audio stream
- // this can be ignored
- }
- else
+ int exitCode;
+
+ using (var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ Arguments = processArgs,
+ FileName = _mediaEncoder.EncoderPath,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ WorkingDirectory = outputFolder,
+ ErrorDialog = false
+ },
+ EnableRaisingEvents = true
+ })
{
- failed = true;
+ _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ process.Start();
- _logger.LogWarning("Deleting extracted attachments {Path} due to failure: {ExitCode}", outputPath, exitCode);
try
{
- Directory.Delete(outputPath);
+ await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
+ exitCode = process.ExitCode;
}
- catch (IOException ex)
+ catch (OperationCanceledException)
{
- _logger.LogError(ex, "Error deleting extracted attachments {Path}", outputPath);
+ process.Kill(true);
+ exitCode = -1;
}
}
- }
- else if (!Directory.Exists(outputPath))
- {
- failed = true;
- }
-
- if (failed)
- {
- _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
- throw new InvalidOperationException(
- string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath));
- }
-
- _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
- }
-
- private async Task<Stream> GetAttachmentStream(
- MediaSourceInfo mediaSource,
- MediaAttachment mediaAttachment,
- CancellationToken cancellationToken)
- {
- var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource, mediaAttachment, cancellationToken).ConfigureAwait(false);
- return AsyncFile.OpenRead(attachmentPath);
- }
-
- private async Task<string> GetReadableFile(
- string mediaPath,
- string inputFile,
- MediaSourceInfo mediaSource,
- MediaAttachment mediaAttachment,
- CancellationToken cancellationToken)
- {
- await CacheAllAttachments(mediaPath, inputFile, mediaSource, cancellationToken).ConfigureAwait(false);
-
- var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, mediaAttachment.Index);
- await ExtractAttachment(inputFile, mediaSource, mediaAttachment.Index, outputPath, cancellationToken)
- .ConfigureAwait(false);
-
- return outputPath;
- }
-
- private async Task CacheAllAttachments(
- string mediaPath,
- string inputFile,
- MediaSourceInfo mediaSource,
- CancellationToken cancellationToken)
- {
- var outputFileLocks = new List<IDisposable>();
- var extractableAttachmentIds = new List<int>();
+ var failed = false;
- try
- {
- foreach (var attachment in mediaSource.MediaAttachments)
+ if (exitCode != 0)
{
- var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, attachment.Index);
-
- var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
-
- if (File.Exists(outputPath))
+ if (isExternal && exitCode == 1)
{
- releaser.Dispose();
- continue;
+ // ffmpeg returns exitCode 1 because there is no video or audio stream
+ // this can be ignored
}
-
- outputFileLocks.Add(releaser);
- extractableAttachmentIds.Add(attachment.Index);
- }
-
- if (extractableAttachmentIds.Count > 0)
- {
- await CacheAllAttachmentsInternal(mediaPath, _mediaEncoder.GetInputArgument(inputFile, mediaSource), mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false);
- }
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "Unable to cache media attachments for File:{File}", mediaPath);
- }
- finally
- {
- outputFileLocks.ForEach(x => x.Dispose());
- }
- }
-
- private async Task CacheAllAttachmentsInternal(
- string mediaPath,
- string inputFile,
- MediaSourceInfo mediaSource,
- List<int> extractableAttachmentIds,
- CancellationToken cancellationToken)
- {
- var outputPaths = new List<string>();
- var processArgs = string.Empty;
-
- foreach (var attachmentId in extractableAttachmentIds)
- {
- var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, attachmentId);
-
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid."));
-
- outputPaths.Add(outputPath);
- processArgs += string.Format(
- CultureInfo.InvariantCulture,
- " -dump_attachment:{0} \"{1}\"",
- attachmentId,
- EncodingUtils.NormalizePath(outputPath));
- }
-
- processArgs += string.Format(
- CultureInfo.InvariantCulture,
- " -i {0} -t 0 -f null null",
- inputFile);
-
- int exitCode;
-
- using (var process = new Process
- {
- StartInfo = new ProcessStartInfo
+ else
{
- Arguments = processArgs,
- FileName = _mediaEncoder.EncoderPath,
- UseShellExecute = false,
- CreateNoWindow = true,
- WindowStyle = ProcessWindowStyle.Hidden,
- ErrorDialog = false
- },
- EnableRaisingEvents = true
- })
- {
- _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
-
- process.Start();
+ failed = true;
- try
- {
- await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
- exitCode = process.ExitCode;
+ _logger.LogWarning("Deleting extracted attachments {Path} due to failure: {ExitCode}", outputFolder, exitCode);
+ try
+ {
+ Directory.Delete(outputFolder);
+ }
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting extracted attachments {Path}", outputFolder);
+ }
+ }
}
- catch (OperationCanceledException)
+ else if (!Directory.Exists(outputFolder))
{
- process.Kill(true);
- exitCode = -1;
+ failed = true;
}
- }
- var failed = false;
-
- if (exitCode == -1)
- {
- failed = true;
-
- foreach (var outputPath in outputPaths)
+ if (failed)
{
- try
- {
- _logger.LogWarning("Deleting extracted media attachment due to failure: {Path}", outputPath);
- _fileSystem.DeleteFile(outputPath);
- }
- catch (FileNotFoundException)
- {
- // ffmpeg failed, so it is normal that one or more expected output files do not exist.
- // There is no need to log anything for the user here.
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error deleting extracted media attachment {Path}", outputPath);
- }
- }
- }
- else
- {
- foreach (var outputPath in outputPaths)
- {
- if (!File.Exists(outputPath))
- {
- _logger.LogError("ffmpeg media attachment extraction failed for {InputPath} to {OutputPath}", inputFile, outputPath);
- failed = true;
- continue;
- }
+ _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputFolder);
- _logger.LogInformation("ffmpeg media attachment extraction completed for {InputPath} to {OutputPath}", inputFile, outputPath);
+ throw new InvalidOperationException(
+ string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputFolder));
}
- }
- if (failed)
- {
- throw new FfmpegException(
- string.Format(CultureInfo.InvariantCulture, "ffmpeg media attachment extraction failed for {0}", inputFile));
+ _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputFolder);
}
}
- private async Task ExtractAttachment(
+ private async Task<Stream> GetAttachmentStream(
+ MediaSourceInfo mediaSource,
+ MediaAttachment mediaAttachment,
+ CancellationToken cancellationToken)
+ {
+ var attachmentPath = await ExtractAttachment(mediaSource.Path, mediaSource, mediaAttachment, cancellationToken)
+ .ConfigureAwait(false);
+ return AsyncFile.OpenRead(attachmentPath);
+ }
+
+ private async Task<string> ExtractAttachment(
string inputFile,
MediaSourceInfo mediaSource,
- int attachmentStreamIndex,
- string outputPath,
+ MediaAttachment mediaAttachment,
CancellationToken cancellationToken)
{
- using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
+ var attachmentFolderPath = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ using (await _semaphoreLocks.LockAsync(attachmentFolderPath, cancellationToken).ConfigureAwait(false))
{
- if (!File.Exists(outputPath))
+ var attachmentPath = _pathManager.GetAttachmentPath(mediaSource.Id, mediaAttachment.FileName ?? mediaAttachment.Index.ToString(CultureInfo.InvariantCulture));
+ if (!File.Exists(attachmentPath))
{
await ExtractAttachmentInternal(
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
- attachmentStreamIndex,
- outputPath,
+ mediaAttachment.Index,
+ attachmentPath,
cancellationToken).ConfigureAwait(false);
}
+
+ return attachmentPath;
}
}
@@ -510,23 +341,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
_logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
}
- private string GetAttachmentCachePath(string mediaPath, MediaSourceInfo mediaSource, int attachmentStreamIndex)
- {
- string filename;
- if (mediaSource.Protocol == MediaProtocol.File)
- {
- var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
- filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
- }
- else
- {
- filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
- }
-
- var prefix = filename.AsSpan(0, 1);
- return Path.Join(_appPaths.DataPath, "attachments", prefix, filename);
- }
-
/// <inheritdoc />
public void Dispose()
{