aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBond-009 <bond.009@outlook.com>2026-06-03 18:16:35 +0200
committerGitHub <noreply@github.com>2026-06-03 18:16:35 +0200
commit5ee9e79da21b67dc75c918b126d29fcfd09ffd89 (patch)
tree7d5c32293404d5cd659bb0fe168bc5eb0077385a
parent5ed7798c3628a789a9334bc83944c9988ef3c522 (diff)
parente627c723e29804e8f6f682bb61032908961f8699 (diff)
Merge pull request #16915 from Shadowghost/batch-attachment-extract
Extract attachments in one ffmpeg command when dumping
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs147
1 files changed, 140 insertions, 7 deletions
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index d9cb7a450f..9dd3dcecba 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -1,8 +1,10 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
@@ -102,13 +104,10 @@ namespace MediaBrowser.MediaEncoding.Attachments
&& (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
if (shouldExtractOneByOne && !inputFile.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
{
- foreach (var attachment in mediaSource.MediaAttachments)
- {
- if (!string.Equals(attachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
- {
- await ExtractAttachment(inputFile, mediaSource, attachment, cancellationToken).ConfigureAwait(false);
- }
- }
+ await ExtractAllAttachmentsIndividuallyInternal(
+ inputFile,
+ mediaSource,
+ cancellationToken).ConfigureAwait(false);
}
else
{
@@ -119,6 +118,140 @@ namespace MediaBrowser.MediaEncoding.Attachments
}
}
+ private async Task ExtractAllAttachmentsIndividuallyInternal(
+ string inputFile,
+ MediaSourceInfo mediaSource,
+ CancellationToken cancellationToken)
+ {
+ var inputPath = _mediaEncoder.GetInputArgument(inputFile, mediaSource);
+
+ ArgumentException.ThrowIfNullOrEmpty(inputPath);
+
+ var outputFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ if (outputFolder is null)
+ {
+ _logger.LogDebug("Skipping attachment extraction for input {InputFile}: MediaSource Id is not a GUID.", inputFile);
+ return;
+ }
+
+ using (await _semaphoreLocks.LockAsync(outputFolder, cancellationToken).ConfigureAwait(false))
+ {
+ Directory.CreateDirectory(outputFolder);
+
+ var dumpArgs = new StringBuilder();
+ var missingPaths = new List<string>();
+ foreach (var attachment in mediaSource.MediaAttachments)
+ {
+ if (string.Equals(attachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ var indexName = attachment.Index.ToString(CultureInfo.InvariantCulture);
+ var attachmentPath = _pathManager.GetAttachmentPath(mediaSource.Id, attachment.FileName ?? indexName)
+ ?? _pathManager.GetAttachmentPath(mediaSource.Id, indexName)!;
+ if (File.Exists(attachmentPath))
+ {
+ continue;
+ }
+
+ dumpArgs.AppendFormat(
+ CultureInfo.InvariantCulture,
+ "-dump_attachment:{0} \"{1}\" ",
+ attachment.Index,
+ EncodingUtils.NormalizePath(attachmentPath));
+ missingPaths.Add(attachmentPath);
+ }
+
+ if (missingPaths.Count == 0)
+ {
+ // Skip extraction if all files already exist
+ return;
+ }
+
+ var hasVideoOrAudioStream = mediaSource.MediaStreams
+ .Any(s => s.Type == MediaStreamType.Video || s.Type == MediaStreamType.Audio);
+ var processArgs = string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}{1} -i {2} {3}",
+ dumpArgs,
+ inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
+ inputPath,
+ hasVideoOrAudioStream ? "-t 0 -f null null" : string.Empty);
+
+ int exitCode;
+
+ using (var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ 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();
+
+ try
+ {
+ await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
+ exitCode = process.ExitCode;
+ }
+ catch (OperationCanceledException)
+ {
+ process.Kill(true);
+ exitCode = -1;
+ }
+ }
+
+ var failed = false;
+
+ if (exitCode != 0 && (hasVideoOrAudioStream || exitCode != 1))
+ {
+ failed = true;
+
+ foreach (var path in missingPaths)
+ {
+ if (!File.Exists(path))
+ {
+ continue;
+ }
+
+ try
+ {
+ _fileSystem.DeleteFile(path);
+ }
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting extracted attachment {Path}", path);
+ }
+ }
+ }
+
+ if (!failed && missingPaths.Exists(p => !File.Exists(p)))
+ {
+ failed = true;
+ }
+
+ if (failed)
+ {
+ _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputFolder);
+
+ throw new InvalidOperationException(
+ string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputFolder));
+ }
+
+ _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputFolder);
+ }
+ }
+
private async Task ExtractAllAttachmentsInternal(
string inputFile,
MediaSourceInfo mediaSource,